forked from vikunja/vikunja
Compare commits
22 Commits
1416_remin
...
main
Author | SHA1 | Date |
---|---|---|
cernst | 300100ad8a | |
cernst | f45648a6f7 | |
renovate | 9443fb1bd5 | |
renovate | 5114f53307 | |
cernst | 3f5252dc24 | |
kolaente | 823c817b1f | |
renovate | 84c3d0ef6d | |
kolaente | f4e12dab27 | |
kolaente | f0dcce702f | |
kolaente | 9590b82c11 | |
renovate | 7987efcefc | |
cernst | 5961e56d16 | |
kolaente | 33f0d0f85a | |
kolaente | 4d5ad8f50e | |
renovate | f6e6c5c8fc | |
WofWca | 6aadaaaffc | |
renovate | 6566f0e81d | |
renovate | 6d8db0ce1e | |
renovate | 085b9222bb | |
kolaente | a0b3a444df | |
kolaente | 8916de0366 | |
renovate | 769db0dab2 |
|
@ -141,7 +141,7 @@ steps:
|
|||
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.51.2
|
||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.1
|
||||
- ./mage-static check:all
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
@ -506,7 +506,7 @@ steps:
|
|||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm:v2.26.0
|
||||
image: goreleaser/nfpm:v2.27.1
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
|
@ -522,7 +522,7 @@ steps:
|
|||
depends_on: [ after-build-compress ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm:v2.26.0
|
||||
image: goreleaser/nfpm:v2.27.1
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
|
@ -731,6 +731,6 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 7242860ad70556ffeb8fc804ce0ffa0d3d1aa8e0d9167ad476aa392d7e937d48
|
||||
hmac: 166caa5ba66cd55bc0f1c5cb42be0a0a647fbadf66716778cf795fc084fc80fd
|
||||
|
||||
...
|
||||
|
|
|
@ -79,6 +79,7 @@ issues:
|
|||
- path: pkg/routes/api/v1/docs.go
|
||||
linters:
|
||||
- goheader
|
||||
- misspell
|
||||
- text: "Missed string"
|
||||
linters:
|
||||
- goheader
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.20.0 AS builder
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.20.x AS builder
|
||||
|
||||
RUN go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
|
|
|
@ -168,6 +168,10 @@ log:
|
|||
events: "off"
|
||||
# The log level for event log messages. Possible values (case-insensitive) are ERROR, INFO, DEBUG.
|
||||
eventslevel: "info"
|
||||
# Whether or not to log mail log messages. This will not log mail contents. Possible values are stdout, stderr, file or off to disable mail-related logging.
|
||||
mail: "off"
|
||||
# The log level for mail log messages. Possible values (case-insensitive) are ERROR, WARNING, INFO, DEBUG.
|
||||
maillevel: "info"
|
||||
|
||||
ratelimit:
|
||||
# whether or not to enable the rate limit
|
||||
|
|
|
@ -871,6 +871,28 @@ Full path: `log.eventslevel`
|
|||
Environment path: `VIKUNJA_LOG_EVENTSLEVEL`
|
||||
|
||||
|
||||
### mail
|
||||
|
||||
Whether or not to log mail log messages. This will not log mail contents. Possible values are stdout, stderr, file or off to disable mail-related logging.
|
||||
|
||||
Default: `off`
|
||||
|
||||
Full path: `log.mail`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_MAIL`
|
||||
|
||||
|
||||
### maillevel
|
||||
|
||||
The log level for mail log messages. Possible values (case-insensitive) are ERROR, WARNING, INFO, DEBUG.
|
||||
|
||||
Default: `info`
|
||||
|
||||
Full path: `log.maillevel`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_MAILLEVEL`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ratelimit
|
||||
|
|
|
@ -39,30 +39,31 @@ Vikunja currently supports the following properties:
|
|||
* `PRIORITY`
|
||||
* `CATEGORIES`
|
||||
* `COMPLETED`
|
||||
* `CREATED` (only Vikunja -> Client)
|
||||
* `DUE`
|
||||
* `DTSTART`
|
||||
* `DURATION`
|
||||
* `ORGANIZER`
|
||||
* `RELATED-TO`
|
||||
* `CREATED`
|
||||
* `DTSTAMP`
|
||||
* `LAST-MODIFIED`
|
||||
* Recurrence
|
||||
* `DTSTART`
|
||||
* `LAST-MODIFIED` (only Vikunja -> Client)
|
||||
* `RRULE` (Recurrence) (only Vikunja -> Client)
|
||||
* `VALARM` (Reminders)
|
||||
|
||||
Vikunja **currently does not** support these properties:
|
||||
|
||||
* `ATTACH`
|
||||
* `CLASS`
|
||||
* `COMMENT`
|
||||
* `CONTACT`
|
||||
* `GEO`
|
||||
* `LOCATION`
|
||||
* `ORGANIZER` (disabled)
|
||||
* `PERCENT-COMPLETE`
|
||||
* `RESOURCES`
|
||||
* `STATUS`
|
||||
* `CONTACT`
|
||||
* `RECURRENCE-ID`
|
||||
* `URL`
|
||||
* `RELATED-TO`
|
||||
* `RESOURCES`
|
||||
* `SEQUENCE`
|
||||
* `STATUS`
|
||||
* `URL`
|
||||
|
||||
## Tested Clients
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ This document describes the different errors Vikunja can return.
|
|||
| 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. |
|
||||
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
|
||||
|
||||
## Namespace
|
||||
|
||||
|
|
18
go.mod
18
go.mod
|
@ -21,7 +21,7 @@ require (
|
|||
gitea.com/xorm/xorm-redis-cache v0.2.0
|
||||
github.com/ThreeDotsLabs/watermill v1.2.0
|
||||
github.com/adlio/trello v1.10.0
|
||||
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0
|
||||
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||
|
@ -31,15 +31,15 @@ require (
|
|||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/getsentry/sentry-go v0.19.0
|
||||
github.com/getsentry/sentry-go v0.20.0
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1
|
||||
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a
|
||||
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/imdario/mergo v0.3.13
|
||||
github.com/imdario/mergo v0.3.15
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/labstack/echo-jwt/v4 v4.1.0
|
||||
github.com/labstack/echo/v4 v4.10.2
|
||||
|
@ -58,11 +58,11 @@ require (
|
|||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/swaggo/swag v1.8.10
|
||||
github.com/swaggo/swag v1.8.12
|
||||
github.com/tkuchiki/go-timezone v0.2.2
|
||||
github.com/ulule/limiter/v3 v3.11.1
|
||||
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae
|
||||
github.com/wneessen/go-mail v0.3.8
|
||||
github.com/wneessen/go-mail v0.3.9
|
||||
github.com/yuin/goldmark v1.5.4
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/image v0.6.0
|
||||
|
@ -138,13 +138,13 @@ require (
|
|||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/mod v0.9.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
google.golang.org/protobuf v1.29.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
|
22
go.sum
22
go.sum
|
@ -78,6 +78,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
|
|||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0 h1:VVPogIxPiZ6WK5G4Pve5VSQ4HEFiJ8GChpqRjo1gN2c=
|
||||
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
|
||||
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc h1:up1aDcTCZ3KrL2ukKxNqjMRx/CCaXyn9Wl6N7ea3EWc=
|
||||
github.com/arran4/golang-ical v0.0.0-20230318005454-19abf92700cc/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
|
@ -179,6 +181,8 @@ github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkN
|
|||
github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ=
|
||||
github.com/getsentry/sentry-go v0.19.0 h1:BcCH3CN5tXt5aML+gwmbFwVptLLQA+eT866fCO9wVOM=
|
||||
github.com/getsentry/sentry-go v0.19.0/go.mod h1:y3+lGEFEFexZtpbG1GUE2WD/f9zGyKYwpEqryTOC/nE=
|
||||
github.com/getsentry/sentry-go v0.20.0 h1:bwXW98iMRIWxn+4FgPW7vMrjmbym6HblXALmhjHmQaQ=
|
||||
github.com/getsentry/sentry-go v0.20.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
|
@ -217,6 +221,8 @@ github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWl
|
|||
github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA=
|
||||
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a h1:/5o1ejt5M0fNAN2lU1NBLtPzUSZru689EWJq01ptr+E=
|
||||
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479 h1:KaCpc4e48emF9hYmMB9INyfpGJHAZxEAS9EqWFkpTig=
|
||||
github.com/gocarina/gocsv v0.0.0-20230325173030-9a18a846a479/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
|
@ -348,6 +354,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/imdario/mergo v0.3.14 h1:fOqeC1+nCuuk6PKQdg9YmosXX7Y7mHX6R/0ZldI9iHo=
|
||||
github.com/imdario/mergo v0.3.14/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
|
@ -684,6 +694,10 @@ github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8
|
|||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
|
||||
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/swaggo/swag v1.8.11 h1:Fp1dNNtDvbCf+8kvehZbHQnlF6AxHGjmw6H/xAMrZfY=
|
||||
github.com/swaggo/swag v1.8.11/go.mod h1:2GXgpNI9iy5OdsYWu8zXfRAGnOAPxYxTWTyM0XOTYZQ=
|
||||
github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w=
|
||||
github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=
|
||||
|
@ -711,6 +725,8 @@ github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae h1:oyiy
|
|||
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae/go.mod h1:PnwzbSst7KD3vpBzzlntZU5gjVa455Uqa5QPiKSYJzQ=
|
||||
github.com/wneessen/go-mail v0.3.8 h1:ja5D/o/RVwrtRIYFlrO7GmtcjDNeMakGQuwQRZYv0JM=
|
||||
github.com/wneessen/go-mail v0.3.8/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E=
|
||||
github.com/wneessen/go-mail v0.3.9 h1:Q4DbCk3htT5DtDWKeMgNXCiHc4bBY/vv/XQPT6XDXzc=
|
||||
github.com/wneessen/go-mail v0.3.9/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
@ -805,6 +821,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -1038,6 +1056,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
|||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -1148,6 +1168,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
|||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
|
||||
google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -405,7 +405,7 @@ func checkGolangCiLintInstalled() {
|
|||
mg.Deps(initVars)
|
||||
if err := exec.Command("golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
|
||||
fmt.Println("Please manually install golangci-lint by running")
|
||||
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.51.2")
|
||||
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.1")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,19 +31,6 @@ import (
|
|||
// DateFormat is the caldav date format
|
||||
const DateFormat = `20060102T150405`
|
||||
|
||||
// Event holds a single caldav event
|
||||
type Event struct {
|
||||
Summary string
|
||||
Description string
|
||||
UID string
|
||||
Alarms []Alarm
|
||||
Color string
|
||||
|
||||
Timestamp time.Time
|
||||
Start time.Time
|
||||
End time.Time
|
||||
}
|
||||
|
||||
// Todo holds a single VTODO
|
||||
type Todo struct {
|
||||
// Required
|
||||
|
@ -65,6 +52,7 @@ type Todo struct {
|
|||
Duration time.Duration
|
||||
RepeatAfter int64
|
||||
RepeatMode models.TaskRepeatMode
|
||||
Alarms []Alarm
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time // last-mod
|
||||
|
@ -73,6 +61,8 @@ type Todo struct {
|
|||
// Alarm holds infos about an alarm from a caldav event
|
||||
type Alarm struct {
|
||||
Time time.Time
|
||||
Duration time.Duration
|
||||
RelativeTo models.ReminderRelation
|
||||
Description string
|
||||
}
|
||||
|
||||
|
@ -100,58 +90,6 @@ X-OUTLOOK-COLOR:` + color + `
|
|||
X-FUNAMBOL-COLOR:` + color
|
||||
}
|
||||
|
||||
// ParseEvents parses an array of caldav events and gives them back as string
|
||||
func ParseEvents(config *Config, events []*Event) (caldavevents string) {
|
||||
caldavevents += `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:` + config.Name + `
|
||||
PRODID:-//` + config.ProdID + `//EN` + getCaldavColor(config.Color)
|
||||
|
||||
for _, e := range events {
|
||||
|
||||
if e.UID == "" {
|
||||
e.UID = makeCalDavTimeFromTimeStamp(e.Timestamp) + utils.Sha256(e.Summary)
|
||||
}
|
||||
|
||||
formattedDescription := ""
|
||||
if e.Description != "" {
|
||||
re := regexp.MustCompile(`\r?\n`)
|
||||
formattedDescription = re.ReplaceAllString(e.Description, "\\n")
|
||||
}
|
||||
|
||||
caldavevents += `
|
||||
BEGIN:VEVENT
|
||||
UID:` + e.UID + `
|
||||
SUMMARY:` + e.Summary + getCaldavColor(e.Color) + `
|
||||
DESCRIPTION:` + formattedDescription + `
|
||||
DTSTAMP:` + makeCalDavTimeFromTimeStamp(e.Timestamp) + `
|
||||
DTSTART:` + makeCalDavTimeFromTimeStamp(e.Start) + `
|
||||
DTEND:` + makeCalDavTimeFromTimeStamp(e.End)
|
||||
|
||||
for _, a := range e.Alarms {
|
||||
if a.Description == "" {
|
||||
a.Description = e.Summary
|
||||
}
|
||||
|
||||
caldavevents += `
|
||||
BEGIN:VALARM
|
||||
TRIGGER:` + calcAlarmDateFromReminder(e.Start, a.Time) + `
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:` + a.Description + `
|
||||
END:VALARM`
|
||||
}
|
||||
caldavevents += `
|
||||
END:VEVENT`
|
||||
}
|
||||
|
||||
caldavevents += `
|
||||
END:VCALENDAR` // Need a line break
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func formatDuration(duration time.Duration) string {
|
||||
seconds := duration.Seconds() - duration.Minutes()*60
|
||||
minutes := duration.Minutes() - duration.Hours()*60
|
||||
|
@ -246,7 +184,7 @@ CATEGORIES:` + strings.Join(t.Categories, ",")
|
|||
|
||||
caldavtodos += `
|
||||
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
|
||||
|
||||
caldavtodos += ParseAlarms(t.Alarms, t.Summary)
|
||||
caldavtodos += `
|
||||
END:VTODO`
|
||||
}
|
||||
|
@ -257,19 +195,42 @@ END:VCALENDAR` // Need a line break
|
|||
return
|
||||
}
|
||||
|
||||
func ParseAlarms(alarms []Alarm, taskDescription string) (caldavalarms string) {
|
||||
for _, a := range alarms {
|
||||
if a.Description == "" {
|
||||
a.Description = taskDescription
|
||||
}
|
||||
|
||||
caldavalarms += `
|
||||
BEGIN:VALARM`
|
||||
switch a.RelativeTo {
|
||||
case models.ReminderRelationStartDate:
|
||||
caldavalarms += `
|
||||
TRIGGER;RELATED=START:` + makeCalDavDuration(a.Duration)
|
||||
case models.ReminderRelationEndDate, models.ReminderRelationDueDate:
|
||||
caldavalarms += `
|
||||
TRIGGER;RELATED=END:` + makeCalDavDuration(a.Duration)
|
||||
default:
|
||||
caldavalarms += `
|
||||
TRIGGER;VALUE=DATE-TIME:` + makeCalDavTimeFromTimeStamp(a.Time)
|
||||
}
|
||||
caldavalarms += `
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:` + a.Description + `
|
||||
END:VALARM`
|
||||
}
|
||||
return caldavalarms
|
||||
}
|
||||
|
||||
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
|
||||
return ts.In(time.UTC).Format(DateFormat) + "Z"
|
||||
}
|
||||
|
||||
func calcAlarmDateFromReminder(eventStart, reminder time.Time) (alarmTime string) {
|
||||
diff := reminder.Sub(eventStart)
|
||||
diffStr := strings.ToUpper(diff.String())
|
||||
if diff < 0 {
|
||||
alarmTime += `-`
|
||||
// We append the - at the beginning of the caldav flag, that would get in the way if the minutes
|
||||
// themselves are also containing it
|
||||
diffStr = diffStr[1:]
|
||||
func makeCalDavDuration(duration time.Duration) (caldavtime string) {
|
||||
if duration < 0 {
|
||||
duration = duration.Abs()
|
||||
caldavtime = "-"
|
||||
}
|
||||
alarmTime += `PT` + diffStr
|
||||
caldavtime += "PT" + strings.ToUpper(duration.Truncate(time.Millisecond).String())
|
||||
return
|
||||
}
|
||||
|
|
|
@ -26,275 +26,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseEvents(t *testing.T) {
|
||||
type args struct {
|
||||
config *Config
|
||||
events []*Event
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantCaldavevents string
|
||||
}{
|
||||
{
|
||||
name: "Test caldavparsing without reminders",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
Color: "ffffff",
|
||||
},
|
||||
events: []*Event{
|
||||
{
|
||||
Summary: "Event #1",
|
||||
Description: "Lorem Ipsum",
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
|
||||
Color: "affffe",
|
||||
},
|
||||
{
|
||||
Summary: "Event #2",
|
||||
UID: "randommduidd",
|
||||
Timestamp: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
||||
Start: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
||||
End: time.Unix(1543738724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
{
|
||||
Summary: "Event #3 with empty uid",
|
||||
UID: "20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83",
|
||||
Timestamp: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
||||
Start: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
||||
End: time.Unix(1543727000, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavevents: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
X-APPLE-CALENDAR-COLOR:#ffffffFF
|
||||
X-OUTLOOK-COLOR:#ffffffFF
|
||||
X-FUNAMBOL-COLOR:#ffffffFF
|
||||
BEGIN:VEVENT
|
||||
UID:randommduid
|
||||
SUMMARY:Event #1
|
||||
X-APPLE-CALENDAR-COLOR:#affffeFF
|
||||
X-OUTLOOK-COLOR:#affffeFF
|
||||
X-FUNAMBOL-COLOR:#affffeFF
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
DTSTAMP:20181201T011204Z
|
||||
DTSTART:20181201T011204Z
|
||||
DTEND:20181201T013024Z
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:randommduidd
|
||||
SUMMARY:Event #2
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T045844Z
|
||||
DTSTART:20181202T045844Z
|
||||
DTEND:20181202T081844Z
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
|
||||
SUMMARY:Event #3 with empty uid
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T050024Z
|
||||
DTSTART:20181202T050024Z
|
||||
DTEND:20181202T050320Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "Test caldavparsing with reminders",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test2",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
},
|
||||
events: []*Event{
|
||||
{
|
||||
Summary: "Event #1",
|
||||
Description: "Lorem Ipsum",
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
|
||||
Alarms: []Alarm{
|
||||
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
|
||||
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
|
||||
{Time: time.Unix(1543626024, 0)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Summary: "Event #2",
|
||||
UID: "randommduidd",
|
||||
Timestamp: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
||||
Start: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
||||
End: time.Unix(1543738724, 0).In(config.GetTimeZone()),
|
||||
Alarms: []Alarm{
|
||||
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
|
||||
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
|
||||
{Time: time.Unix(1543626024, 0).In(config.GetTimeZone())},
|
||||
},
|
||||
},
|
||||
{
|
||||
Summary: "Event #3 with empty uid",
|
||||
Timestamp: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
||||
Start: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
||||
End: time.Unix(1543727000, 0).In(config.GetTimeZone()),
|
||||
Alarms: []Alarm{
|
||||
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
|
||||
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
|
||||
{Time: time.Unix(1543626024, 0).In(config.GetTimeZone())},
|
||||
{Time: time.Unix(1543826824, 0).In(config.GetTimeZone())},
|
||||
},
|
||||
},
|
||||
{
|
||||
Summary: "Event #4 without any",
|
||||
Timestamp: time.Unix(1543726824, 0),
|
||||
Start: time.Unix(1543726824, 0),
|
||||
End: time.Unix(1543727000, 0),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavevents: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test2
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VEVENT
|
||||
UID:randommduid
|
||||
SUMMARY:Event #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
DTSTAMP:20181201T011204Z
|
||||
DTSTART:20181201T011204Z
|
||||
DTEND:20181201T013024Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT3M20S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #1
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT8M20S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #1
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT11M40S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #1
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:randommduidd
|
||||
SUMMARY:Event #2
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T045844Z
|
||||
DTSTART:20181202T045844Z
|
||||
DTEND:20181202T081844Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT27H50M0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #2
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT27H55M0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #2
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT27H58M20S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #2
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20181202T050024Z2aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
|
||||
SUMMARY:Event #3 with empty uid
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T050024Z
|
||||
DTSTART:20181202T050024Z
|
||||
DTEND:20181202T050320Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT27H51M40S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #3 with empty uid
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT27H56M40S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #3 with empty uid
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT28H0M0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #3 with empty uid
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:PT27H46M40S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Event #3 with empty uid
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20181202T050024Zae7548ce9556df85038abe90dc674d4741a61ce74d1cf
|
||||
SUMMARY:Event #4 without any
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T050024Z
|
||||
DTSTART:20181202T050024Z
|
||||
DTEND:20181202T050320Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "Test caldavparsing with multiline description",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
},
|
||||
events: []*Event{
|
||||
{
|
||||
Summary: "Event #1",
|
||||
Description: `Lorem Ipsum
|
||||
Dolor sit amet`,
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavevents: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VEVENT
|
||||
UID:randommduid
|
||||
SUMMARY:Event #1
|
||||
DESCRIPTION:Lorem Ipsum\nDolor sit amet
|
||||
DTSTAMP:20181201T011204Z
|
||||
DTSTART:20181201T011204Z
|
||||
DTEND:20181201T013024Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotCaldavevents := ParseEvents(tt.args.config, tt.args.events)
|
||||
assert.Equal(t, gotCaldavevents, tt.wantCaldavevents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTodos(t *testing.T) {
|
||||
type args struct {
|
||||
config *Config
|
||||
|
@ -520,13 +251,88 @@ X-FUNAMBOL-COLOR:#affffeFF
|
|||
CATEGORIES:label1,label2
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "with alarm",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
},
|
||||
todos: []*Todo{
|
||||
{
|
||||
Summary: "Todo #1",
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Alarms: []Alarm{
|
||||
{
|
||||
Time: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
{
|
||||
Time: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Description: "alarm description",
|
||||
},
|
||||
{
|
||||
Duration: -2 * time.Hour,
|
||||
RelativeTo: "due_date",
|
||||
},
|
||||
{
|
||||
Duration: 1 * time.Hour,
|
||||
RelativeTo: "start_date",
|
||||
},
|
||||
{
|
||||
Duration: time.Duration(0),
|
||||
RelativeTo: "end_date",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DATE-TIME:20181201T011204Z
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Todo #1
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DATE-TIME:20181201T011204Z
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:alarm description
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;RELATED=END:-PT2H0M0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Todo #1
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;RELATED=START:PT1H0M0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Todo #1
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;RELATED=END:PT0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Todo #1
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotCaldavtasks := ParseTodos(tt.args.config, tt.args.todos)
|
||||
assert.Equal(t, gotCaldavtasks, tt.wantCaldavtasks)
|
||||
assert.Equal(t, tt.wantCaldavtasks, gotCaldavtasks)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,15 @@
|
|||
package caldav
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
ics "github.com/arran4/golang-ical"
|
||||
)
|
||||
|
@ -38,6 +41,14 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
|||
for _, label := range t.Labels {
|
||||
categories = append(categories, label.Title)
|
||||
}
|
||||
var alarms []Alarm
|
||||
for _, reminder := range t.Reminders {
|
||||
alarms = append(alarms, Alarm{
|
||||
Time: reminder.Reminder,
|
||||
Duration: time.Duration(reminder.RelativePeriod) * time.Second,
|
||||
RelativeTo: reminder.RelativeTo,
|
||||
})
|
||||
}
|
||||
|
||||
caldavtodos = append(caldavtodos, &Todo{
|
||||
Timestamp: t.Updated,
|
||||
|
@ -56,6 +67,7 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
|||
Duration: duration,
|
||||
RepeatAfter: t.RepeatAfter,
|
||||
RepeatMode: t.RepeatMode,
|
||||
Alarms: alarms,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -72,17 +84,20 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We put the task details in a map to be able to handle them more easily
|
||||
task := make(map[string]string)
|
||||
for _, c := range parsed.Components[0].UnknownPropertiesIANAProperties() {
|
||||
task[c.IANAToken] = c.Value
|
||||
vTodo, ok := parsed.Components[0].(*ics.VTodo)
|
||||
if !ok {
|
||||
return nil, errors.New("VTODO element not found")
|
||||
}
|
||||
// We put the vTodo details in a map to be able to handle them more easily
|
||||
task := make(map[string]ics.IANAProperty)
|
||||
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
|
||||
task[c.IANAToken] = c
|
||||
}
|
||||
|
||||
// Parse the priority
|
||||
var priority int64
|
||||
if _, ok := task["PRIORITY"]; ok {
|
||||
priorityParsed, err := strconv.ParseInt(task["PRIORITY"], 10, 64)
|
||||
priorityParsed, err := strconv.ParseInt(task["PRIORITY"].Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -91,14 +106,14 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
}
|
||||
|
||||
// Parse the enddate
|
||||
duration, _ := time.ParseDuration(task["DURATION"])
|
||||
duration, _ := time.ParseDuration(task["DURATION"].Value)
|
||||
|
||||
description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",")
|
||||
description := strings.ReplaceAll(task["DESCRIPTION"].Value, "\\,", ",")
|
||||
description = strings.ReplaceAll(description, "\\n", "\n")
|
||||
|
||||
var labels []*models.Label
|
||||
if val, ok := task["CATEGORIES"]; ok {
|
||||
categories := strings.Split(val, ",")
|
||||
categories := strings.Split(val.Value, ",")
|
||||
labels = make([]*models.Label, 0, len(categories))
|
||||
for _, category := range categories {
|
||||
labels = append(labels, &models.Label{
|
||||
|
@ -108,8 +123,8 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
}
|
||||
|
||||
vTask = &models.Task{
|
||||
UID: task["UID"],
|
||||
Title: task["SUMMARY"],
|
||||
UID: task["UID"].Value,
|
||||
Title: task["SUMMARY"].Value,
|
||||
Description: description,
|
||||
Priority: priority,
|
||||
Labels: labels,
|
||||
|
@ -119,7 +134,7 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
|
||||
}
|
||||
|
||||
if task["STATUS"] == "COMPLETED" {
|
||||
if task["STATUS"].Value == "COMPLETED" {
|
||||
vTask.Done = true
|
||||
}
|
||||
|
||||
|
@ -127,11 +142,66 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
vTask.EndDate = vTask.StartDate.Add(duration)
|
||||
}
|
||||
|
||||
for _, vAlarm := range vTodo.SubComponents() {
|
||||
if vAlarm, ok := vAlarm.(*ics.VAlarm); ok {
|
||||
vTask = parseVAlarm(vAlarm, vTask)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) *models.Task {
|
||||
for _, property := range vAlarm.UnknownPropertiesIANAProperties() {
|
||||
if property.IANAToken != "TRIGGER" {
|
||||
continue
|
||||
}
|
||||
|
||||
if contains(property.ICalParameters["VALUE"], "DATE-TIME") {
|
||||
// Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
||||
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||
Reminder: caldavTimeToTimestamp(property),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
duration := utils.ParseISO8601Duration(property.Value)
|
||||
|
||||
if contains(property.ICalParameters["RELATED"], "END") {
|
||||
// Example: TRIGGER;RELATED=END:-P2D
|
||||
if vTask.EndDate.IsZero() {
|
||||
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||
RelativePeriod: int64(duration.Seconds()),
|
||||
RelativeTo: models.ReminderRelationDueDate})
|
||||
} else {
|
||||
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||
RelativePeriod: int64(duration.Seconds()),
|
||||
RelativeTo: models.ReminderRelationEndDate})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Example: TRIGGER;RELATED=START:-P2D
|
||||
// Example: TRIGGER:-PT60M
|
||||
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||
RelativePeriod: int64(duration.Seconds()),
|
||||
RelativeTo: models.ReminderRelationStartDate})
|
||||
}
|
||||
return vTask
|
||||
}
|
||||
|
||||
func contains(array []string, str string) bool {
|
||||
for _, value := range array {
|
||||
if value == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc5545#section-3.3.5
|
||||
func caldavTimeToTimestamp(tstring string) time.Time {
|
||||
func caldavTimeToTimestamp(ianaProperty ics.IANAProperty) time.Time {
|
||||
tstring := ianaProperty.Value
|
||||
if tstring == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
|
@ -146,7 +216,24 @@ func caldavTimeToTimestamp(tstring string) time.Time {
|
|||
format = `20060102`
|
||||
}
|
||||
|
||||
t, err := time.Parse(format, tstring)
|
||||
var t time.Time
|
||||
var err error
|
||||
tzParameter := ianaProperty.ICalParameters["TZID"]
|
||||
if len(tzParameter) > 0 {
|
||||
loc, err := time.LoadLocation(tzParameter[0])
|
||||
if err != nil {
|
||||
log.Warningf("Error while parsing caldav timezone %s: %s", tzParameter[0], err)
|
||||
} else {
|
||||
t, err = time.ParseInLocation(format, tstring, loc)
|
||||
if err != nil {
|
||||
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s at location %s", tstring, loc, err)
|
||||
} else {
|
||||
t = t.In(config.GetTimeZone())
|
||||
return t
|
||||
}
|
||||
}
|
||||
}
|
||||
t, err = time.Parse(format, tstring)
|
||||
if err != nil {
|
||||
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err)
|
||||
return time.Time{}
|
||||
|
|
|
@ -118,6 +118,177 @@ END:VCALENDAR`,
|
|||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With alarm (time trigger)",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
||||
ACTION:DISPLAY
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Title: "Todo #1",
|
||||
UID: "randomuid",
|
||||
Description: "Lorem Ipsum",
|
||||
Reminders: []*models.TaskReminder{
|
||||
{
|
||||
Reminder: time.Date(2018, 12, 1, 1, 12, 10, 0, config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With alarm (relative trigger)",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
DTSTART:20230228T170000Z
|
||||
DUE:20230304T150000Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:PT0S
|
||||
ACTION:DISPLAY
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DURATION:-PT60M
|
||||
ACTION:DISPLAY
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT61M
|
||||
ACTION:DISPLAY
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;RELATED=START:-P1D
|
||||
ACTION:DISPLAY
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;RELATED=END:-PT30M
|
||||
ACTION:DISPLAY
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Title: "Todo #1",
|
||||
UID: "randomuid",
|
||||
Description: "Lorem Ipsum",
|
||||
StartDate: time.Date(2023, 2, 28, 17, 0, 0, 0, config.GetTimeZone()),
|
||||
DueDate: time.Date(2023, 3, 4, 15, 0, 0, 0, config.GetTimeZone()),
|
||||
Reminders: []*models.TaskReminder{
|
||||
{
|
||||
RelativeTo: models.ReminderRelationStartDate,
|
||||
RelativePeriod: 0,
|
||||
},
|
||||
{
|
||||
RelativeTo: models.ReminderRelationStartDate,
|
||||
RelativePeriod: -3600,
|
||||
},
|
||||
{
|
||||
RelativeTo: models.ReminderRelationStartDate,
|
||||
RelativePeriod: -3660,
|
||||
},
|
||||
{
|
||||
RelativeTo: models.ReminderRelationStartDate,
|
||||
RelativePeriod: -86400,
|
||||
},
|
||||
{
|
||||
RelativeTo: models.ReminderRelationDueDate,
|
||||
RelativePeriod: -1800,
|
||||
},
|
||||
},
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "example task from tasks.org app",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:+//IDN tasks.org//android-130102//EN
|
||||
BEGIN:VTODO
|
||||
DTSTAMP:20230402T074158Z
|
||||
UID:4290517349243274514
|
||||
CREATED:20230402T060451Z
|
||||
LAST-MODIFIED:20230402T074154Z
|
||||
SUMMARY:Test with tasks.org
|
||||
PRIORITY:9
|
||||
CATEGORIES:Vikunja
|
||||
X-APPLE-SORT-ORDER:697384109
|
||||
DUE;TZID=Europe/Berlin:20230402T170001
|
||||
DTSTART;TZID=Europe/Berlin:20230401T090000
|
||||
BEGIN:VALARM
|
||||
TRIGGER;RELATED=END:PT0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Default Tasks.org description
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DATE-TIME:20230402T100000Z
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Default Tasks.org description
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
LAST-MODIFIED:20220816T024022Z
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19810329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19961027T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Updated: time.Date(2023, 4, 2, 7, 41, 58, 0, config.GetTimeZone()),
|
||||
UID: "4290517349243274514",
|
||||
Title: "Test with tasks.org",
|
||||
Priority: 1,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Vikunja",
|
||||
},
|
||||
},
|
||||
DueDate: time.Date(2023, 4, 2, 15, 0, 1, 0, config.GetTimeZone()),
|
||||
StartDate: time.Date(2023, 4, 1, 7, 0, 0, 0, config.GetTimeZone()),
|
||||
Reminders: []*models.TaskReminder{
|
||||
{
|
||||
RelativeTo: models.ReminderRelationDueDate,
|
||||
RelativePeriod: 0,
|
||||
},
|
||||
{
|
||||
Reminder: time.Date(2023, 4, 2, 10, 0, 0, 0, config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -127,7 +298,7 @@ END:VCALENDAR`,
|
|||
return
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.wantVTask); !equal {
|
||||
t.Errorf("ParseTaskFromVTODO() gotVTask = %v, want %v, diff = %s", got, tt.wantVTask, diff)
|
||||
t.Errorf("ParseTaskFromVTODO()\n gotVTask = %v\n want %v\n diff = %s", got, tt.wantVTask, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -175,6 +346,16 @@ func TestGetCaldavTodosForTasks(t *testing.T) {
|
|||
Title: "label2",
|
||||
},
|
||||
},
|
||||
Reminders: []*models.TaskReminder{
|
||||
{
|
||||
Reminder: time.Unix(1543626730, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
{
|
||||
Reminder: time.Unix(1543626731, 0).In(config.GetTimeZone()),
|
||||
RelativePeriod: -3600,
|
||||
RelativeTo: models.ReminderRelationDueDate,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -200,6 +381,16 @@ PRIORITY:3
|
|||
RRULE:FREQ=SECONDLY;INTERVAL=86400
|
||||
CATEGORIES:label1,label2
|
||||
LAST-MODIFIED:20181201T011205Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Task 1
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
TRIGGER;RELATED=END:-PT1H0M0S
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Task 1
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
|
|
|
@ -118,6 +118,8 @@ const (
|
|||
LogPath Key = `log.path`
|
||||
LogEvents Key = `log.events`
|
||||
LogEventsLevel Key = `log.eventslevel`
|
||||
LogMail Key = `log.mail`
|
||||
LogMailLevel Key = `log.maillevel`
|
||||
|
||||
RateLimitEnabled Key = `ratelimit.enabled`
|
||||
RateLimitKind Key = `ratelimit.kind`
|
||||
|
@ -351,6 +353,8 @@ func InitDefaultConfig() {
|
|||
LogPath.setDefault(ServiceRootpath.GetString() + "/logs")
|
||||
LogEvents.setDefault("off")
|
||||
LogEventsLevel.setDefault("INFO")
|
||||
LogMail.setDefault("off")
|
||||
LogMailLevel.setDefault("INFO")
|
||||
// Rate Limit
|
||||
RateLimitEnabled.setDefault(false)
|
||||
RateLimitKind.setDefault("user")
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
task_id: 27
|
||||
reminder: 2018-12-01 01:13:44
|
||||
created: 2018-12-01 01:12:04
|
||||
relative_to: 'start_date'
|
||||
relative_period: -3600
|
||||
- id: 3
|
||||
task_id: 2
|
||||
reminder: 2018-12-01 01:13:44
|
||||
created: 2018-12-01 01:12:04
|
||||
- id: 4
|
||||
task_id: 39
|
||||
reminder: 2023-03-04 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
|
|
|
@ -244,7 +244,7 @@
|
|||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 27
|
||||
title: 'task #27 with reminders'
|
||||
title: 'task #27 with reminders and start_date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
project_id: 1
|
||||
|
@ -252,6 +252,7 @@
|
|||
bucket_id: 1
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
start_date: 2018-11-30 22:25:24
|
||||
- id: 28
|
||||
title: 'task #28 with repeat after'
|
||||
done: false
|
||||
|
|
|
@ -37,6 +37,10 @@ SUMMARY:Caldav Task 1
|
|||
CATEGORIES:tag1,tag2,tag3
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER;VALUE=DATE-TIME:20230304T150000Z
|
||||
ACTION:DISPLAY
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
|
@ -65,5 +69,9 @@ func TestCaldav(t *testing.T) {
|
|||
assert.Contains(t, rec.Body.String(), "DUE:20230301T150000Z")
|
||||
assert.Contains(t, rec.Body.String(), "PRIORITY:3")
|
||||
assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4")
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VALARM")
|
||||
assert.Contains(t, rec.Body.String(), "TRIGGER;VALUE=DATE-TIME:20230304T150000Z")
|
||||
assert.Contains(t, rec.Body.String(), "ACTION:DISPLAY")
|
||||
assert.Contains(t, rec.Body.String(), "END:VALARM")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -42,14 +42,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
// Not using assert.Equal to avoid having the tests break every time we add new fixtures
|
||||
assert.Contains(t, rec.Body.String(), `task #1`)
|
||||
assert.Contains(t, rec.Body.String(), `task #2`)
|
||||
assert.Contains(t, rec.Body.String(), `task #3`)
|
||||
assert.Contains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.Contains(t, rec.Body.String(), `task #2 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #3 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #4 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #10`)
|
||||
assert.Contains(t, rec.Body.String(), `task #11`)
|
||||
assert.Contains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -75,14 +75,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #7 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -93,14 +93,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"tASk #6"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #7 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -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,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
// Due date without unix suffix
|
||||
t.Run("by duedate asc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by due_date without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
|
@ -171,10 +171,10 @@ func TestTaskCollection(t *testing.T) {
|
|||
// Invalid parameter should not sort at all
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
})
|
||||
t.Run("Filter", func(t *testing.T) {
|
||||
|
@ -190,14 +190,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -215,14 +215,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -255,14 +255,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -291,14 +291,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -314,14 +314,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
// Not using assert.Equal to avoid having the tests break every time we add new fixtures
|
||||
assert.Contains(t, rec.Body.String(), `task #1`)
|
||||
assert.Contains(t, rec.Body.String(), `task #2`)
|
||||
assert.Contains(t, rec.Body.String(), `task #3`)
|
||||
assert.Contains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.Contains(t, rec.Body.String(), `task #2 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #3 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #4 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #10`)
|
||||
assert.Contains(t, rec.Body.String(), `task #11`)
|
||||
assert.Contains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -347,14 +347,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #7 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -366,42 +366,42 @@ 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,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
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,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
})
|
||||
t.Run("Filter", func(t *testing.T) {
|
||||
|
@ -417,14 +417,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.Contains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #5 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
@ -442,14 +442,14 @@ func TestTaskCollection(t *testing.T) {
|
|||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #6 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #7 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8 `)
|
||||
assert.Contains(t, rec.Body.String(), `task #9 `)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
|
|
|
@ -95,24 +95,47 @@ func TestTask(t *testing.T) {
|
|||
assert.Contains(t, rec.Body.String(), `"due_date":"0001-01-01T00:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`)
|
||||
})
|
||||
t.Run("Reminders", func(t *testing.T) {
|
||||
// Deprecated: Remove if ReminderDates is removed
|
||||
t.Run("ReminderDates", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminder_dates":["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]`)
|
||||
assert.NotContains(t, rec.Body.String(), `"reminder_dates": null`)
|
||||
})
|
||||
t.Run("Reminders unset to empty array", func(t *testing.T) {
|
||||
// Deprecated: Remove if ReminderDates is removed
|
||||
t.Run("ReminderDates unset to empty array", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": []}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
|
||||
})
|
||||
t.Run("Reminders unset to null", func(t *testing.T) {
|
||||
// Deprecated: Remove if ReminderDates is removed
|
||||
t.Run("ReminderDates unset to null", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminder_dates": null}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`)
|
||||
})
|
||||
t.Run("Reminders", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"reminders": [{"reminder": "2020-02-10T10:00:00Z"},{"reminder": "2020-02-11T10:00:00Z"}]}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminders":[`)
|
||||
assert.Contains(t, rec.Body.String(), `{"reminder":"2020-02-10T10:00:00Z"`)
|
||||
assert.Contains(t, rec.Body.String(), `{"reminder":"2020-02-11T10:00:00Z"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"reminders":null`)
|
||||
})
|
||||
t.Run("Reminders unset to empty array", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminders": []}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminders":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"`)
|
||||
})
|
||||
t.Run("Reminders unset to null", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "27"}, `{"reminders": null}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"reminder_dates":null`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"`)
|
||||
})
|
||||
t.Run("Repeat after", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"projecttask": "1"}, `{"repeat_after":3600}`)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
// 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 log
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/op/go-logging"
|
||||
"xorm.io/xorm/log"
|
||||
)
|
||||
|
||||
type MailLogger struct {
|
||||
logger *logging.Logger
|
||||
level log.LogLevel
|
||||
}
|
||||
|
||||
const mailFormat = `%{color}%{time:` + time.RFC3339Nano + `}: %{level}` + "\t" + `▶ [MAIL] %{id:03x}%{color:reset} %{message}`
|
||||
const mailLogModule = `vikunja_mail`
|
||||
|
||||
func NewMailLogger() *MailLogger {
|
||||
lvl := strings.ToUpper(config.LogMailLevel.GetString())
|
||||
level, err := logging.LogLevel(lvl)
|
||||
if err != nil {
|
||||
Criticalf("Error setting database log level: %s", err.Error())
|
||||
}
|
||||
|
||||
mailLogger := &MailLogger{
|
||||
logger: logging.MustGetLogger(mailLogModule),
|
||||
}
|
||||
|
||||
logBackend := logging.NewLogBackend(GetLogWriter("mail"), "", 0)
|
||||
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(mailFormat+"\n"))
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(level, mailLogModule)
|
||||
|
||||
mailLogger.logger.SetBackend(backendLeveled)
|
||||
|
||||
switch level {
|
||||
case logging.CRITICAL:
|
||||
case logging.ERROR:
|
||||
mailLogger.level = log.LOG_ERR
|
||||
case logging.WARNING:
|
||||
mailLogger.level = log.LOG_WARNING
|
||||
case logging.NOTICE:
|
||||
case logging.INFO:
|
||||
mailLogger.level = log.LOG_INFO
|
||||
case logging.DEBUG:
|
||||
mailLogger.level = log.LOG_DEBUG
|
||||
default:
|
||||
mailLogger.level = log.LOG_OFF
|
||||
}
|
||||
|
||||
return mailLogger
|
||||
}
|
||||
|
||||
func (m *MailLogger) Errorf(format string, v ...interface{}) {
|
||||
m.logger.Errorf(format, v...)
|
||||
}
|
||||
|
||||
func (m *MailLogger) Warnf(format string, v ...interface{}) {
|
||||
m.logger.Warningf(format, v...)
|
||||
}
|
||||
|
||||
func (m *MailLogger) Infof(format string, v ...interface{}) {
|
||||
m.logger.Infof(format, v...)
|
||||
}
|
||||
|
||||
func (m *MailLogger) Debugf(format string, v ...interface{}) {
|
||||
m.logger.Debugf(format, v...)
|
||||
}
|
|
@ -23,6 +23,6 @@ import (
|
|||
// NoopBackend doesn't log anything. Used in cases where we want to disable logging completely.
|
||||
type NoopBackend struct{}
|
||||
|
||||
func (n *NoopBackend) Log(level logging.Level, i int, record *logging.Record) error {
|
||||
func (n *NoopBackend) Log(_ logging.Level, _ int, _ *logging.Record) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -91,6 +91,6 @@ func (w *WatermillLogger) Trace(msg string, fields watermill.LogFields) {
|
|||
w.logger.Debugf("%s, %s", msg, concatFields(fields))
|
||||
}
|
||||
|
||||
func (w *WatermillLogger) With(fields watermill.LogFields) watermill.LoggerAdapter {
|
||||
func (w *WatermillLogger) With(_ watermill.LogFields) watermill.LoggerAdapter {
|
||||
return w
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ func getClient() (*mail.Client, error) {
|
|||
ServerName: config.MailerHost.GetString(),
|
||||
}),
|
||||
mail.WithTimeout((config.MailerQueueTimeout.GetDuration() + 3) * time.Second), // 3s more for us to close before mail server timeout
|
||||
mail.WithLogger(log.NewMailLogger()),
|
||||
mail.WithDebugLog(),
|
||||
}
|
||||
|
||||
if config.MailerForceSSL.GetBool() {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// 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 taskReminders20230307171848 struct {
|
||||
RelativePeriod int64 `xorm:"bigint null" json:"relative_period"`
|
||||
RelativeTo string `xorm:"varchar(50) null" json:"relative_to,omitempty"`
|
||||
}
|
||||
|
||||
func (taskReminders20230307171848) TableName() string {
|
||||
return "task_reminders"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20230307171848",
|
||||
Description: "Add relative period to task reminders",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(taskReminders20230307171848{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -109,10 +109,7 @@ func Rollback(migrationID string) {
|
|||
// MigrateTo executes all migrations up to a certain point
|
||||
func MigrateTo(migrationID string, x *xorm.Engine) error {
|
||||
m := initMigration(x)
|
||||
if err := m.MigrateTo(migrationID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return m.MigrateTo(migrationID)
|
||||
}
|
||||
|
||||
// Deletes a column from a table. All arguments are strings, to let them be standalone and not depending on any struct.
|
||||
|
|
|
@ -875,6 +875,33 @@ func (err ErrUserAlreadyAssigned) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrReminderRelativeToMissing represents an error where a task has a relative reminder without reference date
|
||||
type ErrReminderRelativeToMissing struct {
|
||||
TaskID int64
|
||||
}
|
||||
|
||||
// IsErrReminderRelativeToMissing checks if an error is ErrReminderRelativeToMissing.
|
||||
func IsErrReminderRelativeToMissing(err error) bool {
|
||||
_, ok := err.(ErrReminderRelativeToMissing)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrReminderRelativeToMissing) Error() string {
|
||||
return fmt.Sprintf("Task [TaskID: %v] has a relative reminder without relative_to", err.TaskID)
|
||||
}
|
||||
|
||||
// ErrCodeRelationDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeReminderRelativeToMissing = 4022
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrReminderRelativeToMissing) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeReminderRelativeToMissing,
|
||||
Message: "Please provide what the reminder date is relative to",
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
|
|
@ -281,7 +281,7 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{projectID}/buckets/{bucketID} [post]
|
||||
func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
doneBucket, err := getDoneBucketForProject(s, b.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -320,7 +320,7 @@ func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{projectID}/buckets/{bucketID} [delete]
|
||||
func (b *Bucket) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (b *Bucket) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Prevent removing the last bucket
|
||||
total, err := s.Where("project_id = ?", b.ProjectID).Count(&Bucket{})
|
||||
|
|
|
@ -123,7 +123,7 @@ func (l *Label) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "Label not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels/{id} [delete]
|
||||
func (l *Label) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (l *Label) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
_, err = s.ID(l.ID).Delete(&Label{})
|
||||
return err
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
|
|||
// @Failure 404 {object} web.HTTPError "Label not found"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels/{id} [get]
|
||||
func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (l *Label) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
|
||||
label, err := getLabelByIDSimple(s, l.ID)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
@ -40,7 +40,7 @@ func (l *Label) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
|||
|
||||
// CanCreate checks if the user can create a label
|
||||
// Currently a dummy.
|
||||
func (l *Label) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
func (l *Label) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ func (LabelTask) TableName() string {
|
|||
// @Failure 404 {object} web.HTTPError "Label not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{task}/labels/{label} [delete]
|
||||
func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (lt *LabelTask) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
_, err = s.Delete(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
|
||||
return err
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "The label does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{task}/labels [put]
|
||||
func (lt *LabelTask) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (lt *LabelTask) Create(s *xorm.Session, _ web.Auth) (err error) {
|
||||
// Check if the label is already added
|
||||
exists, err := s.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
|
||||
if err != nil {
|
||||
|
@ -118,7 +118,7 @@ func (lt *LabelTask) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Success 200 {array} models.Label "The labels"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{task}/labels [get]
|
||||
func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page int, _ int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
// Check if the user has the right to see the task
|
||||
task := Task{ID: lt.TaskID}
|
||||
canRead, _, err := task.CanRead(s, a)
|
||||
|
|
|
@ -169,7 +169,7 @@ func (share *LinkSharing) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "Share Link not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{project}/shares/{share} [get]
|
||||
func (share *LinkSharing) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (share *LinkSharing) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
|
||||
exists, err := s.Where("id = ?", share.ID).Get(share)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -269,7 +269,7 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
// @Failure 404 {object} web.HTTPError "Share Link not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{project}/shares/{share} [delete]
|
||||
func (share *LinkSharing) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (share *LinkSharing) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
_, err = s.Where("id = ?", share.ID).Delete(share)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ func (s *IncreaseTaskCounter) Name() string {
|
|||
}
|
||||
|
||||
// Handle is executed when the event IncreaseTaskCounter listens on is fired
|
||||
func (s *IncreaseTaskCounter) Handle(msg *message.Message) (err error) {
|
||||
func (s *IncreaseTaskCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ func (s *DecreaseTaskCounter) Name() string {
|
|||
}
|
||||
|
||||
// Handle is executed when the event DecreaseTaskCounter listens on is fired
|
||||
func (s *DecreaseTaskCounter) Handle(msg *message.Message) (err error) {
|
||||
func (s *DecreaseTaskCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
|
||||
|
@ -480,7 +480,7 @@ func (s *IncreaseProjectCounter) Name() string {
|
|||
return "project.counter.increase"
|
||||
}
|
||||
|
||||
func (s *IncreaseProjectCounter) Handle(msg *message.Message) (err error) {
|
||||
func (s *IncreaseProjectCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.ProjectCountKey, 1)
|
||||
}
|
||||
|
||||
|
@ -491,7 +491,7 @@ func (s *DecreaseProjectCounter) Name() string {
|
|||
return "project.counter.decrease"
|
||||
}
|
||||
|
||||
func (s *DecreaseProjectCounter) Handle(msg *message.Message) (err error) {
|
||||
func (s *DecreaseProjectCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.ProjectCountKey, 1)
|
||||
}
|
||||
|
||||
|
@ -553,7 +553,7 @@ func (s *IncreaseNamespaceCounter) Name() string {
|
|||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseNamespaceCounter listens on is fired
|
||||
func (s *IncreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
|
||||
func (s *IncreaseNamespaceCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.NamespaceCountKey, 1)
|
||||
}
|
||||
|
||||
|
@ -566,8 +566,8 @@ func (s *DecreaseNamespaceCounter) Name() string {
|
|||
return "namespace.counter.decrease"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event DecreaseNamespaceCounter listens on is fired
|
||||
func (s *DecreaseNamespaceCounter) Handle(msg *message.Message) (err error) {
|
||||
// Handle is executed when the event DecreaseNamespaceCounter listens on is fired
|
||||
func (s *DecreaseNamespaceCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.NamespaceCountKey, 1)
|
||||
}
|
||||
|
||||
|
@ -583,8 +583,8 @@ func (s *IncreaseTeamCounter) Name() string {
|
|||
return "team.counter.increase"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseTeamCounter listens on is fired
|
||||
func (s *IncreaseTeamCounter) Handle(msg *message.Message) (err error) {
|
||||
// Handle is executed when the event IncreaseTeamCounter listens on is fired
|
||||
func (s *IncreaseTeamCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.TeamCountKey, 1)
|
||||
}
|
||||
|
||||
|
@ -597,8 +597,8 @@ func (s *DecreaseTeamCounter) Name() string {
|
|||
return "team.counter.decrease"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event DecreaseTeamCounter listens on is fired
|
||||
func (s *DecreaseTeamCounter) Handle(msg *message.Message) (err error) {
|
||||
// Handle is executed when the event DecreaseTeamCounter listens on is fired
|
||||
func (s *DecreaseTeamCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.TeamCountKey, 1)
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ func (n *Namespace) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
}
|
||||
|
||||
// CanCreate checks if the user can create a new namespace
|
||||
func (n *Namespace) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
func (n *Namespace) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ func (tn *TeamNamespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [delete]
|
||||
func (tn *TeamNamespace) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (tn *TeamNamespace) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tn.TeamID)
|
||||
|
@ -229,7 +229,7 @@ func (tn *TeamNamespace) ReadAll(s *xorm.Session, a web.Auth, search string, pag
|
|||
// @Failure 404 {object} web.HTTPError "Team or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/teams/{teamID} [post]
|
||||
func (tn *TeamNamespace) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (tn *TeamNamespace) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tn.Right.isValid(); err != nil {
|
||||
|
|
|
@ -133,7 +133,7 @@ func (nu *NamespaceUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "user or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [delete]
|
||||
func (nu *NamespaceUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (nu *NamespaceUser) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user2.GetUserByUsername(s, nu.Username)
|
||||
|
@ -229,7 +229,7 @@ func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, pag
|
|||
// @Failure 404 {object} web.HTTPError "User or namespace does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/users/{userID} [post]
|
||||
func (nu *NamespaceUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (nu *NamespaceUser) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := nu.Right.isValid(); err != nil {
|
||||
|
|
|
@ -47,7 +47,7 @@ type DatabaseNotifications struct {
|
|||
// @Failure 403 {object} web.HTTPError "Link shares cannot have notifications."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /notifications [get]
|
||||
func (d *DatabaseNotifications) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) {
|
||||
func (d *DatabaseNotifications) ReadAll(s *xorm.Session, a web.Auth, _ string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
@ -79,6 +79,6 @@ func (d *DatabaseNotifications) CanUpdate(s *xorm.Session, a web.Auth) (bool, er
|
|||
// @Failure 404 {object} web.HTTPError "The notification does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /notifications/{id} [post]
|
||||
func (d *DatabaseNotifications) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (d *DatabaseNotifications) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
return notifications.MarkNotificationAsRead(s, &d.DatabaseNotification, d.Read)
|
||||
}
|
||||
|
|
|
@ -135,7 +135,7 @@ func (tl *TeamProject) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "Team or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{projectID}/teams/{teamID} [delete]
|
||||
func (tl *TeamProject) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (tl *TeamProject) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the team exists
|
||||
_, err = GetTeamByID(s, tl.TeamID)
|
||||
|
@ -247,7 +247,7 @@ func (tl *TeamProject) ReadAll(s *xorm.Session, a web.Auth, search string, page
|
|||
// @Failure 404 {object} web.HTTPError "Team or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{projectID}/teams/{teamID} [post]
|
||||
func (tl *TeamProject) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (tl *TeamProject) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := tl.Right.isValid(); err != nil {
|
||||
|
|
|
@ -142,7 +142,7 @@ func (lu *ProjectUser) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "user or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{projectID}/users/{userID} [delete]
|
||||
func (lu *ProjectUser) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (lu *ProjectUser) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the user exists
|
||||
u, err := user.GetUserByUsername(s, lu.Username)
|
||||
|
@ -244,7 +244,7 @@ func (lu *ProjectUser) ReadAll(s *xorm.Session, a web.Auth, search string, page
|
|||
// @Failure 404 {object} web.HTTPError "User or project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{projectID}/users/{userID} [post]
|
||||
func (lu *ProjectUser) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (lu *ProjectUser) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
// Check if the right is valid
|
||||
if err := lu.Right.isValid(); err != nil {
|
||||
|
|
|
@ -149,7 +149,7 @@ func getSavedFilterSimpleByID(s *xorm.Session, id int64) (sf *SavedFilter, err e
|
|||
// @Failure 403 {object} web.HTTPError "The user does not have access to that saved filter."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [get]
|
||||
func (sf *SavedFilter) ReadOne(s *xorm.Session, a web.Auth) error {
|
||||
func (sf *SavedFilter) ReadOne(s *xorm.Session, _ web.Auth) error {
|
||||
// s already contains almost the full saved filter from the rights check, we only need to add the user
|
||||
u, err := user.GetUserByID(s, sf.OwnerID)
|
||||
sf.Owner = u
|
||||
|
@ -169,7 +169,7 @@ func (sf *SavedFilter) ReadOne(s *xorm.Session, a web.Auth) error {
|
|||
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [post]
|
||||
func (sf *SavedFilter) Update(s *xorm.Session, a web.Auth) error {
|
||||
func (sf *SavedFilter) Update(s *xorm.Session, _ web.Auth) error {
|
||||
origFilter, err := getSavedFilterSimpleByID(s, sf.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -204,7 +204,7 @@ func (sf *SavedFilter) Update(s *xorm.Session, a web.Auth) error {
|
|||
// @Failure 404 {object} web.HTTPError "The saved filter does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [delete]
|
||||
func (sf *SavedFilter) Delete(s *xorm.Session, a web.Auth) error {
|
||||
func (sf *SavedFilter) Delete(s *xorm.Session, _ web.Auth) error {
|
||||
_, err := s.
|
||||
Where("id = ?", sf.ID).
|
||||
Delete(sf)
|
||||
|
|
|
@ -40,7 +40,7 @@ func (sf *SavedFilter) CanUpdate(s *xorm.Session, auth web.Auth) (bool, error) {
|
|||
}
|
||||
|
||||
// CanCreate checks if a user has the right to update a saved filter
|
||||
func (sf *SavedFilter) CanCreate(s *xorm.Session, auth web.Auth) (bool, error) {
|
||||
func (sf *SavedFilter) CanCreate(_ *xorm.Session, auth web.Auth) (bool, error) {
|
||||
if _, is := auth.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ func (ta *TaskAttachment) NewAttachment(s *xorm.Session, f io.ReadCloser, realna
|
|||
}
|
||||
|
||||
// ReadOne returns a task attachment
|
||||
func (ta *TaskAttachment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (ta *TaskAttachment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
|
||||
exists, err := s.Where("id = ?", ta.ID).Get(ta)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -127,7 +127,7 @@ func (ta *TaskAttachment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} models.Message "The task does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id}/attachments [get]
|
||||
func (ta *TaskAttachment) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
func (ta *TaskAttachment) ReadAll(s *xorm.Session, _ web.Auth, _ string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
attachments := []*TaskAttachment{}
|
||||
|
||||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
|
|
|
@ -266,6 +266,13 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
|||
return nil, nil, ErrInvalidTaskField{TaskField: fieldName}
|
||||
}
|
||||
|
||||
if realFieldName == "Reminders" {
|
||||
field, ok = reflect.TypeOf(&TaskReminder{}).Elem().FieldByName("Reminder")
|
||||
if !ok {
|
||||
return nil, nil, ErrInvalidTaskField{TaskField: fieldName}
|
||||
}
|
||||
}
|
||||
|
||||
if comparator == taskFilterComparatorIn {
|
||||
vals := strings.Split(value, ",")
|
||||
valueSlice := []interface{}{}
|
||||
|
|
|
@ -169,9 +169,17 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
label4,
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Reminders: []time.Time{
|
||||
ReminderDates: []time.Time{
|
||||
time.Unix(1543626824, 0).In(loc),
|
||||
},
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
ID: 3,
|
||||
TaskID: 2,
|
||||
Reminder: time.Unix(1543626824, 0).In(loc),
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
},
|
||||
},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
}
|
||||
|
@ -472,15 +480,32 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task27 := &Task{
|
||||
ID: 27,
|
||||
Title: "task #27 with reminders",
|
||||
Title: "task #27 with reminders and start_date",
|
||||
Identifier: "test1-12",
|
||||
Index: 12,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
Reminders: []time.Time{
|
||||
ReminderDates: []time.Time{
|
||||
time.Unix(1543626724, 0).In(loc),
|
||||
time.Unix(1543626824, 0).In(loc),
|
||||
},
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
ID: 1,
|
||||
TaskID: 27,
|
||||
Reminder: time.Unix(1543626724, 0).In(loc),
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
TaskID: 27,
|
||||
Reminder: time.Unix(1543626824, 0).In(loc),
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
RelativePeriod: -3600,
|
||||
RelativeTo: "start_date",
|
||||
},
|
||||
},
|
||||
StartDate: time.Unix(1543616724, 0).In(loc),
|
||||
ProjectID: 1,
|
||||
BucketID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
|
@ -906,7 +931,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filtered reminders",
|
||||
name: "filtered reminder dates",
|
||||
fields: fields{
|
||||
FilterBy: []string{"reminders", "reminders"},
|
||||
FilterValue: []string{"2018-10-01T00:00:00+00:00", "2018-12-10T00:00:00+00:00"},
|
||||
|
@ -1246,7 +1271,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
t.Errorf("Test %s, Task.ReadAll() = %v, want %v, \ndiff: %v", tt.name, got, tt.want, diff)
|
||||
t.Errorf("Test %s, Task.ReadAll() = %v, \nwant %v, \ndiff: %v", tt.name, got, tt.want, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 404 {object} web.HTTPError "The task comment was not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments/{commentID} [delete]
|
||||
func (tc *TaskComment) Delete(s *xorm.Session, a web.Auth) error {
|
||||
func (tc *TaskComment) Delete(s *xorm.Session, _ web.Auth) error {
|
||||
deleted, err := s.
|
||||
ID(tc.ID).
|
||||
NoAutoCondition().
|
||||
|
@ -135,7 +135,7 @@ func (tc *TaskComment) Delete(s *xorm.Session, a web.Auth) error {
|
|||
// @Failure 404 {object} web.HTTPError "The task comment was not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments/{commentID} [post]
|
||||
func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
|
||||
func (tc *TaskComment) Update(s *xorm.Session, _ web.Auth) error {
|
||||
updated, err := s.
|
||||
ID(tc.ID).
|
||||
Cols("comment").
|
||||
|
@ -192,7 +192,7 @@ func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
|
|||
// @Failure 404 {object} web.HTTPError "The task comment was not found."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments/{commentID} [get]
|
||||
func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (tc *TaskComment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
|
||||
err = getTaskCommentSimple(s, tc)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -33,12 +33,29 @@ import (
|
|||
"code.vikunja.io/api/pkg/user"
|
||||
)
|
||||
|
||||
// TaskReminder holds a reminder on a task
|
||||
// ReminderRelation represents the date attribute of the task which a period based reminder relates to
|
||||
type ReminderRelation string
|
||||
|
||||
// All valid ReminderRelations
|
||||
const (
|
||||
ReminderRelationDueDate ReminderRelation = `due_date`
|
||||
ReminderRelationStartDate ReminderRelation = `start_date`
|
||||
ReminderRelationEndDate ReminderRelation = `end_date`
|
||||
)
|
||||
|
||||
// TaskReminder holds a reminder on a task.
|
||||
// If RelativeTo and the assciated date field are defined, then the attribute Reminder will be computed.
|
||||
// If RelativeTo is missing, than Reminder must be given.
|
||||
type TaskReminder struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk"`
|
||||
TaskID int64 `xorm:"bigint not null INDEX"`
|
||||
Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'"`
|
||||
Created time.Time `xorm:"created not null"`
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"`
|
||||
TaskID int64 `xorm:"bigint not null INDEX" json:"-"`
|
||||
// The absolute time when the user wants to be reminded of the task.
|
||||
Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'" json:"reminder"`
|
||||
Created time.Time `xorm:"created not null" json:"-"`
|
||||
// A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date. Default: 0, tiggers when RelativeTo is due.
|
||||
RelativePeriod int64 `xorm:"bigint null" json:"relative_period"`
|
||||
// The name of the date field to which the relative period refers to.
|
||||
RelativeTo ReminderRelation `xorm:"varchar(50) null" json:"relative_to"`
|
||||
}
|
||||
|
||||
// TableName returns a pretty table name
|
||||
|
|
|
@ -62,7 +62,11 @@ type Task struct {
|
|||
// The time when the task is due.
|
||||
DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"`
|
||||
// An array of datetimes when the user wants to be reminded of the task.
|
||||
Reminders []time.Time `xorm:"-" json:"reminder_dates"`
|
||||
//
|
||||
// Deprecated: Use Reminders
|
||||
ReminderDates []time.Time `xorm:"-" json:"reminder_dates"`
|
||||
// An array of reminders that are associated with this task.
|
||||
Reminders []*TaskReminder `xorm:"-" json:"reminders"`
|
||||
// The project this task belongs to.
|
||||
ProjectID int64 `xorm:"bigint INDEX not null" json:"project_id" param:"project"`
|
||||
// An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount.
|
||||
|
@ -194,7 +198,7 @@ type taskOptions struct {
|
|||
// @Success 200 {array} models.Task "The tasks"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/all [get]
|
||||
func (t *Task) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
func (t *Task) ReadAll(_ *xorm.Session, _ web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, totalItems int64, err error) {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
|
@ -641,8 +645,8 @@ func addAttachmentsToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*
|
|||
return
|
||||
}
|
||||
|
||||
func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int64][]time.Time, err error) {
|
||||
taskReminders = make(map[int64][]time.Time)
|
||||
func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int64][]*TaskReminder, err error) {
|
||||
taskReminders = make(map[int64][]*TaskReminder)
|
||||
|
||||
// Get all reminders and put them in a map to have it easier later
|
||||
reminders, err := getRemindersForTasks(s, taskIDs)
|
||||
|
@ -651,7 +655,7 @@ func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int
|
|||
}
|
||||
|
||||
for _, r := range reminders {
|
||||
taskReminders[r.TaskID] = append(taskReminders[r.TaskID], r.Reminder)
|
||||
taskReminders[r.TaskID] = append(taskReminders[r.TaskID], r)
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -772,6 +776,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e
|
|||
// Make created by user objects
|
||||
task.CreatedBy = users[task.CreatedByID]
|
||||
|
||||
// Add the reminder dates (Remove, when ReminderDates is removed)
|
||||
for _, r := range taskReminders[task.ID] {
|
||||
task.ReminderDates = append(task.ReminderDates, r.Reminder)
|
||||
}
|
||||
|
||||
// Add the reminders
|
||||
task.Reminders = taskReminders[task.ID]
|
||||
|
||||
|
@ -965,7 +974,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
}
|
||||
|
||||
// Update the reminders
|
||||
if err := t.updateReminders(s, t.Reminders); err != nil {
|
||||
if err := t.updateReminders(s, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1017,15 +1026,19 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
t.ProjectID = ot.ProjectID
|
||||
}
|
||||
|
||||
// Get the reminders
|
||||
// Get the stored reminders
|
||||
reminders, err := getRemindersForTasks(s, []int64{t.ID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ot.Reminders = make([]time.Time, len(reminders))
|
||||
// Old task has the stored reminders
|
||||
ot.Reminders = reminders
|
||||
|
||||
// Deprecated: remove when ReminderDates is removed
|
||||
ot.ReminderDates = make([]time.Time, len(reminders))
|
||||
for i, r := range reminders {
|
||||
ot.Reminders[i] = r.Reminder
|
||||
ot.ReminderDates[i] = r.Reminder
|
||||
}
|
||||
|
||||
targetBucket, err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID)
|
||||
|
@ -1049,7 +1062,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// Update the reminders
|
||||
if err := ot.updateReminders(s, t.Reminders); err != nil {
|
||||
if err := ot.updateReminders(s, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1331,9 +1344,9 @@ func setTaskDatesDefault(oldTask, newTask *Task) {
|
|||
// To make this easier, we sort them first because we can then rely on the fact the first is the smallest
|
||||
if len(oldTask.Reminders) > 0 {
|
||||
for in, r := range oldTask.Reminders {
|
||||
newTask.Reminders[in] = r.Add(repeatDuration)
|
||||
for !newTask.Reminders[in].After(now) {
|
||||
newTask.Reminders[in] = newTask.Reminders[in].Add(repeatDuration)
|
||||
newTask.Reminders[in].Reminder = r.Reminder.Add(repeatDuration)
|
||||
for !newTask.Reminders[in].Reminder.After(now) {
|
||||
newTask.Reminders[in].Reminder = newTask.Reminders[in].Reminder.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1364,7 +1377,7 @@ func setTaskDatesMonthRepeat(oldTask, newTask *Task) {
|
|||
newTask.Reminders = oldTask.Reminders
|
||||
if len(oldTask.Reminders) > 0 {
|
||||
for in, r := range oldTask.Reminders {
|
||||
newTask.Reminders[in] = addOneMonthToDate(r)
|
||||
newTask.Reminders[in].Reminder = addOneMonthToDate(r.Reminder)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1405,12 +1418,12 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
|
|||
// To make this easier, we sort them first because we can then rely on the fact the first is the smallest
|
||||
if len(oldTask.Reminders) > 0 {
|
||||
sort.Slice(oldTask.Reminders, func(i, j int) bool {
|
||||
return oldTask.Reminders[i].Unix() < oldTask.Reminders[j].Unix()
|
||||
return oldTask.Reminders[i].Reminder.Unix() < oldTask.Reminders[j].Reminder.Unix()
|
||||
})
|
||||
first := oldTask.Reminders[0]
|
||||
first := oldTask.Reminders[0].Reminder
|
||||
for in, r := range oldTask.Reminders {
|
||||
diff := r.Sub(first)
|
||||
newTask.Reminders[in] = now.Add(repeatDuration + diff)
|
||||
diff := r.Reminder.Sub(first)
|
||||
newTask.Reminders[in].Reminder = now.Add(repeatDuration + diff)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1478,11 +1491,66 @@ func updateDone(oldTask *Task, newTask *Task) {
|
|||
}
|
||||
}
|
||||
|
||||
// Deprecated: will be removed when ReminderDates are removed from Task.
|
||||
// For now the method just creates TaskReminder objects from the ReminderDates and overwrites Reminder.
|
||||
func (t *Task) overwriteRemindersWithReminderDates(reminderDates []time.Time) {
|
||||
// If the client still sends old reminder_dates, then these will overwrite
|
||||
// the Reminders, if the were sent by the client, too.
|
||||
// We assume that clients still using the old API with reminder_dates do not understand the new reminders.
|
||||
// Clients who want to use the new Reminder structure must explicitey unset reminder_dates.
|
||||
|
||||
// start with empty Reminders
|
||||
reminders := make([]*TaskReminder, 0)
|
||||
|
||||
// append absolute triggers from ReminderDates
|
||||
for _, reminderDate := range reminderDates {
|
||||
reminders = append(reminders, &TaskReminder{TaskID: t.ID, Reminder: reminderDate})
|
||||
}
|
||||
t.Reminders = reminders
|
||||
}
|
||||
|
||||
// Set the absolute trigger dates for Reminders with relative period
|
||||
func updateRelativeReminderDates(task *Task) (err error) {
|
||||
for _, reminder := range task.Reminders {
|
||||
relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second
|
||||
if reminder.RelativeTo != "" {
|
||||
reminder.Reminder = time.Time{}
|
||||
}
|
||||
switch reminder.RelativeTo {
|
||||
case ReminderRelationDueDate:
|
||||
if !task.DueDate.IsZero() {
|
||||
reminder.Reminder = task.DueDate.Add(relativeDuration)
|
||||
}
|
||||
case ReminderRelationStartDate:
|
||||
if !task.StartDate.IsZero() {
|
||||
reminder.Reminder = task.StartDate.Add(relativeDuration)
|
||||
}
|
||||
case ReminderRelationEndDate:
|
||||
if !task.EndDate.IsZero() {
|
||||
reminder.Reminder = task.EndDate.Add(relativeDuration)
|
||||
}
|
||||
default:
|
||||
if reminder.RelativePeriod != 0 {
|
||||
err = ErrReminderRelativeToMissing{
|
||||
TaskID: task.ID,
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes all old reminders and adds the new ones. This is a lot easier and less buggy than
|
||||
// trying to figure out which reminders changed and then only re-add those needed. And since it does
|
||||
// not make a performance difference we'll just do that.
|
||||
// The parameter is a slice with unix dates which holds the new reminders.
|
||||
func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err error) {
|
||||
// The parameter is a slice which holds the new reminders.
|
||||
func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) {
|
||||
|
||||
// Deprecated: This statement must be removed when ReminderDates will be removed
|
||||
if task.ReminderDates != nil {
|
||||
task.overwriteRemindersWithReminderDates(task.ReminderDates)
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("task_id = ?", t.ID).
|
||||
|
@ -1491,23 +1559,43 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro
|
|||
return
|
||||
}
|
||||
|
||||
// Resolve duplicates and sort them
|
||||
reminderMap := make(map[int64]time.Time, len(reminders))
|
||||
for _, reminder := range reminders {
|
||||
reminderMap[reminder.UTC().Unix()] = reminder
|
||||
err = updateRelativeReminderDates(task)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve duplicates and sort them
|
||||
reminderMap := make(map[int64]*TaskReminder, len(task.Reminders))
|
||||
for _, reminder := range task.Reminders {
|
||||
reminderMap[reminder.Reminder.UTC().Unix()] = reminder
|
||||
}
|
||||
|
||||
t.Reminders = make([]*TaskReminder, 0, len(reminderMap))
|
||||
t.ReminderDates = make([]time.Time, 0, len(reminderMap))
|
||||
|
||||
// Loop through all reminders and add them
|
||||
for _, r := range reminderMap {
|
||||
_, err = s.Insert(&TaskReminder{TaskID: t.ID, Reminder: r})
|
||||
taskReminder := &TaskReminder{
|
||||
TaskID: t.ID,
|
||||
Reminder: r.Reminder,
|
||||
RelativePeriod: r.RelativePeriod,
|
||||
RelativeTo: r.RelativeTo}
|
||||
_, err = s.Insert(taskReminder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Reminders = append(t.Reminders, taskReminder)
|
||||
t.ReminderDates = append(t.ReminderDates, taskReminder.Reminder)
|
||||
}
|
||||
|
||||
t.Reminders = reminders
|
||||
if len(reminders) == 0 {
|
||||
// sort reminders
|
||||
sort.Slice(t.Reminders, func(i, j int) bool {
|
||||
return t.Reminders[i].Reminder.Before(t.Reminders[j].Reminder)
|
||||
})
|
||||
|
||||
if len(t.Reminders) == 0 {
|
||||
t.Reminders = nil
|
||||
t.ReminderDates = nil
|
||||
}
|
||||
|
||||
err = updateProjectLastUpdated(s, &Project{ID: t.ProjectID})
|
||||
|
|
|
@ -70,6 +70,48 @@ func TestTask_Create(t *testing.T) {
|
|||
|
||||
events.AssertDispatched(t, &TaskCreatedEvent{})
|
||||
})
|
||||
t.Run("with reminders", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
Title: "Lorem",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ProjectID: 1,
|
||||
DueDate: time.Date(2023, time.March, 7, 22, 5, 0, 0, time.Local),
|
||||
StartDate: time.Date(2023, time.March, 7, 22, 5, 10, 0, time.Local),
|
||||
EndDate: time.Date(2023, time.March, 7, 22, 5, 20, 0, time.Local),
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
RelativeTo: "due_date",
|
||||
RelativePeriod: 1,
|
||||
},
|
||||
{
|
||||
RelativeTo: "start_date",
|
||||
RelativePeriod: -2,
|
||||
},
|
||||
{
|
||||
RelativeTo: "end_date",
|
||||
RelativePeriod: -1,
|
||||
},
|
||||
{
|
||||
Reminder: time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local),
|
||||
},
|
||||
}}
|
||||
err := task.Create(s, usr)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder)
|
||||
assert.Equal(t, int64(1), task.Reminders[0].RelativePeriod)
|
||||
assert.Equal(t, ReminderRelationDueDate, task.Reminders[0].RelativeTo)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder)
|
||||
assert.Equal(t, ReminderRelationStartDate, task.Reminders[1].RelativeTo)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder)
|
||||
assert.Equal(t, ReminderRelationEndDate, task.Reminders[2].RelativeTo)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("empty title", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
@ -98,7 +140,7 @@ func TestTask_Create(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.True(t, IsErrProjectDoesNotExist(err))
|
||||
})
|
||||
t.Run("noneixtant user", func(t *testing.T) {
|
||||
t.Run("nonexistant user", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
@ -368,7 +410,51 @@ func TestTask_Update(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), task.Index)
|
||||
})
|
||||
t.Run("the same date multiple times should be saved once", func(t *testing.T) {
|
||||
|
||||
t.Run("reminders will be updated", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
ProjectID: 1,
|
||||
Title: "test",
|
||||
DueDate: time.Date(2023, time.March, 7, 22, 5, 0, 0, time.Local),
|
||||
StartDate: time.Date(2023, time.March, 7, 22, 5, 10, 0, time.Local),
|
||||
EndDate: time.Date(2023, time.March, 7, 22, 5, 20, 0, time.Local),
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
RelativeTo: "due_date",
|
||||
RelativePeriod: 1,
|
||||
},
|
||||
{
|
||||
RelativeTo: "start_date",
|
||||
RelativePeriod: -2,
|
||||
},
|
||||
{
|
||||
RelativeTo: "end_date",
|
||||
RelativePeriod: -1,
|
||||
},
|
||||
{
|
||||
Reminder: time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local),
|
||||
},
|
||||
}}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder)
|
||||
assert.Equal(t, int64(1), task.Reminders[0].RelativePeriod)
|
||||
assert.Equal(t, ReminderRelationDueDate, task.Reminders[0].RelativeTo)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder)
|
||||
assert.Equal(t, ReminderRelationStartDate, task.Reminders[1].RelativeTo)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder)
|
||||
assert.Equal(t, ReminderRelationEndDate, task.Reminders[2].RelativeTo)
|
||||
assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 4)
|
||||
})
|
||||
t.Run("the same reminder multiple times should be saved once", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
@ -376,9 +462,13 @@ func TestTask_Update(t *testing.T) {
|
|||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test",
|
||||
Reminders: []time.Time{
|
||||
time.Unix(1674745156, 0),
|
||||
time.Unix(1674745156, 223),
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
Reminder: time.Unix(1674745156, 0),
|
||||
},
|
||||
{
|
||||
Reminder: time.Unix(1674745156, 223),
|
||||
},
|
||||
},
|
||||
ProjectID: 1,
|
||||
}
|
||||
|
@ -386,9 +476,42 @@ func TestTask_Update(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 1)
|
||||
})
|
||||
t.Run("update relative reminder when start_date changes", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// given task with start_date and relative reminder for start_date
|
||||
taskBefore := &Task{
|
||||
Title: "test",
|
||||
ProjectID: 1,
|
||||
StartDate: time.Date(2022, time.March, 8, 8, 5, 20, 0, time.Local),
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
RelativeTo: "start_date",
|
||||
RelativePeriod: -60,
|
||||
},
|
||||
}}
|
||||
err := taskBefore.Create(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, time.Date(2022, time.March, 8, 8, 4, 20, 0, time.Local), taskBefore.Reminders[0].Reminder)
|
||||
|
||||
// when start_date is modified
|
||||
task := taskBefore
|
||||
task.StartDate = time.Date(2023, time.March, 8, 8, 5, 0, 0, time.Local)
|
||||
task.ReminderDates = nil
|
||||
err = task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// then reminder time is updated
|
||||
assert.Equal(t, time.Date(2023, time.March, 8, 8, 4, 0, 0, time.Local), task.Reminders[0].Reminder)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTask_Delete(t *testing.T) {
|
||||
|
@ -487,9 +610,13 @@ func TestUpdateDone(t *testing.T) {
|
|||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
Reminders: []time.Time{
|
||||
time.Unix(1550000000, 0),
|
||||
time.Unix(1555000000, 0),
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
Reminder: time.Unix(1550000000, 0),
|
||||
},
|
||||
{
|
||||
Reminder: time.Unix(1555000000, 0),
|
||||
},
|
||||
},
|
||||
}
|
||||
newTask := &Task{
|
||||
|
@ -507,8 +634,8 @@ func TestUpdateDone(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Len(t, newTask.Reminders, 2)
|
||||
assert.Equal(t, expected1, newTask.Reminders[0])
|
||||
assert.Equal(t, expected2, newTask.Reminders[1])
|
||||
assert.Equal(t, expected1, newTask.Reminders[0].Reminder)
|
||||
assert.Equal(t, expected2, newTask.Reminders[1].Reminder)
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("update start date", func(t *testing.T) {
|
||||
|
@ -585,22 +712,25 @@ func TestUpdateDone(t *testing.T) {
|
|||
Done: false,
|
||||
RepeatAfter: 8600,
|
||||
RepeatMode: TaskRepeatModeFromCurrentDate,
|
||||
Reminders: []time.Time{
|
||||
time.Unix(1550000000, 0),
|
||||
time.Unix(1555000000, 0),
|
||||
},
|
||||
}
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
Reminder: time.Unix(1550000000, 0),
|
||||
},
|
||||
{
|
||||
Reminder: time.Unix(1555000000, 0),
|
||||
},
|
||||
}}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
}
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
diff := oldTask.Reminders[1].Sub(oldTask.Reminders[0])
|
||||
diff := oldTask.Reminders[1].Reminder.Sub(oldTask.Reminders[0].Reminder)
|
||||
|
||||
assert.Len(t, newTask.Reminders, 2)
|
||||
// Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value
|
||||
assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[0].Unix())
|
||||
assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[1].Unix())
|
||||
assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[0].Reminder.Unix())
|
||||
assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[1].Reminder.Unix())
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
t.Run("start date", func(t *testing.T) {
|
||||
|
@ -678,23 +808,28 @@ func TestUpdateDone(t *testing.T) {
|
|||
oldTask := &Task{
|
||||
Done: false,
|
||||
RepeatMode: TaskRepeatModeMonth,
|
||||
Reminders: []time.Time{
|
||||
time.Unix(1550000000, 0),
|
||||
time.Unix(1555000000, 0),
|
||||
},
|
||||
}
|
||||
Reminders: []*TaskReminder{
|
||||
{
|
||||
Reminder: time.Unix(1550000000, 0),
|
||||
},
|
||||
{
|
||||
Reminder: time.Unix(1555000000, 0),
|
||||
},
|
||||
}}
|
||||
newTask := &Task{
|
||||
Done: true,
|
||||
}
|
||||
oldReminders := make([]time.Time, len(oldTask.Reminders))
|
||||
copy(oldReminders, oldTask.Reminders)
|
||||
for i, r := range newTask.Reminders {
|
||||
oldReminders[i] = r.Reminder
|
||||
}
|
||||
|
||||
updateDone(oldTask, newTask)
|
||||
|
||||
assert.Len(t, newTask.Reminders, len(oldReminders))
|
||||
for i, r := range newTask.Reminders {
|
||||
assert.True(t, r.After(oldReminders[i]))
|
||||
assert.NotEqual(t, oldReminders[i].Month(), r.Month())
|
||||
assert.True(t, r.Reminder.After(oldReminders[i]))
|
||||
assert.NotEqual(t, oldReminders[i].Month(), r.Reminder.Month())
|
||||
}
|
||||
assert.False(t, newTask.Done)
|
||||
})
|
||||
|
|
|
@ -88,7 +88,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Success 200 {object} models.Message "The user was successfully removed from the team."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id}/members/{userID} [delete]
|
||||
func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (tm *TeamMember) Delete(s *xorm.Session, _ web.Auth) (err error) {
|
||||
|
||||
total, err := s.Where("team_id = ?", tm.TeamID).Count(&TeamMember{})
|
||||
if err != nil {
|
||||
|
@ -120,7 +120,7 @@ func (tm *TeamMember) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Success 200 {object} models.Message "The member right was successfully changed."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id}/members/{userID}/admin [post]
|
||||
func (tm *TeamMember) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (tm *TeamMember) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
// Find the numeric user id
|
||||
user, err := user2.GetUserByUsername(s, tm.Username)
|
||||
if err != nil {
|
||||
|
|
|
@ -184,7 +184,7 @@ func addMoreInfoToTeams(s *xorm.Session, teams []*Team) (err error) {
|
|||
// @Failure 403 {object} web.HTTPError "The user does not have access to the team"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id} [get]
|
||||
func (t *Team) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (t *Team) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
|
||||
team, err := GetTeamByID(s, t.ID)
|
||||
if team != nil {
|
||||
*t = *team
|
||||
|
@ -338,7 +338,7 @@ func (t *Team) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 400 {object} web.HTTPError "Invalid team object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /teams/{id} [post]
|
||||
func (t *Team) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (t *Team) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
// Check if we have a name
|
||||
if t.Name == "" {
|
||||
return ErrTeamNameCannotBeEmpty{}
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
)
|
||||
|
||||
// CanCreate checks if the user can create a new team
|
||||
func (t *Team) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
func (t *Team) CanCreate(_ *xorm.Session, a web.Auth) (bool, error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -40,6 +40,6 @@ const defaultAvatar string = `<?xml version="1.0" encoding="UTF-8"?>
|
|||
</svg>`
|
||||
|
||||
// GetAvatar implements getting the avatar method
|
||||
func (p *Provider) GetAvatar(user *user.User, size int64) (avatar []byte, mimeType string, err error) {
|
||||
func (p *Provider) GetAvatar(_ *user.User, _ int64) (avatar []byte, mimeType string, err error) {
|
||||
return []byte(defaultAvatar), "image/svg+xml", nil
|
||||
}
|
||||
|
|
|
@ -151,7 +151,7 @@ func getUnsplashPhotoInfoByID(photoID string) (photo *Photo, err error) {
|
|||
// @Success 200 {array} background.Image "An array with photos"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /backgrounds/unsplash/search [get]
|
||||
func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []*background.Image, err error) {
|
||||
func (p *Provider) Search(_ *xorm.Session, search string, page int64) (result []*background.Image, err error) {
|
||||
|
||||
// If we don't have a search query, return results from the unsplash featured collection
|
||||
if search == "" {
|
||||
|
|
|
@ -32,7 +32,7 @@ type Provider struct {
|
|||
}
|
||||
|
||||
// Search is only used to implement the interface
|
||||
func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []*background.Image, err error) {
|
||||
func (p *Provider) Search(_ *xorm.Session, _ string, _ int64) (result []*background.Image, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
|
|||
// @Failure 404 {object} models.Message "The project does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id}/backgrounds/upload [put]
|
||||
func (p *Provider) Set(s *xorm.Session, img *background.Image, project *models.Project, auth web.Auth) (err error) {
|
||||
func (p *Provider) Set(s *xorm.Session, img *background.Image, project *models.Project, _ web.Auth) (err error) {
|
||||
// Remove the old background if one exists
|
||||
err = project.DeleteBackgroundFileIfExists()
|
||||
if err != nil {
|
||||
|
|
|
@ -329,7 +329,9 @@ func convertMicrosoftTodoData(todoData []*project) (vikunjsStructure []*models.N
|
|||
return nil, err
|
||||
}
|
||||
|
||||
task.Reminders = []time.Time{reminder}
|
||||
task.Reminders = []*models.TaskReminder{
|
||||
{Reminder: reminder},
|
||||
}
|
||||
}
|
||||
|
||||
// Due Date
|
||||
|
|
|
@ -141,8 +141,10 @@ func TestConverting(t *testing.T) {
|
|||
{
|
||||
Task: models.Task{
|
||||
Title: "Task 5",
|
||||
Reminders: []time.Time{
|
||||
testtimeTime,
|
||||
Reminders: []*models.TaskReminder{
|
||||
{
|
||||
Reminder: testtimeTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -20,9 +20,7 @@ import (
|
|||
"encoding/csv"
|
||||
"errors"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -30,6 +28,7 @@ import (
|
|||
"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"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
)
|
||||
|
@ -75,36 +74,6 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
|
|||
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.NamespaceWithProjectsAndTasks) {
|
||||
namespace := &models.NamespaceWithProjectsAndTasks{
|
||||
Namespace: models.Namespace{
|
||||
|
@ -147,8 +116,11 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.Namespace
|
|||
}
|
||||
|
||||
if !t.DueDate.IsZero() && t.Reminder > 0 {
|
||||
task.Task.Reminders = []time.Time{
|
||||
t.DueDate.Add(t.Reminder * -1),
|
||||
task.Task.Reminders = []*models.TaskReminder{
|
||||
{
|
||||
RelativeTo: models.ReminderRelationDueDate,
|
||||
RelativePeriod: int64((t.Reminder * -1).Seconds()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,7 +200,7 @@ func (m *Migrator) Migrate(user *user.User, file io.ReaderAt, size int64) error
|
|||
task.IsChecklist = true
|
||||
}
|
||||
|
||||
reminder := parseDuration(task.ReminderDuration)
|
||||
reminder := utils.ParseISO8601Duration(task.ReminderDuration)
|
||||
if reminder > 0 {
|
||||
task.Reminder = reminder
|
||||
}
|
||||
|
|
|
@ -101,7 +101,8 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
|
|||
{Title: "label1"},
|
||||
{Title: "label2"},
|
||||
})
|
||||
//assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Reminders[0].RelativeTo, models.ReminderRelation("due_date"))
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Reminders[0].RelativePeriod, int64(-24*3600))
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Position, tickTickTasks[0].Order)
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Done, false)
|
||||
|
||||
|
@ -127,7 +128,8 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
|
|||
{Title: "label2"},
|
||||
{Title: "other label"},
|
||||
})
|
||||
//assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Reminders[0].RelativeTo, models.ReminderRelation("due_date"))
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Reminders[0].RelativePeriod, int64(-24*3600))
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Position, tickTickTasks[2].Order)
|
||||
assert.Equal(t, vikunjaTasks[0].Projects[0].Tasks[2].Done, false)
|
||||
|
||||
|
|
|
@ -471,7 +471,10 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi
|
|||
return nil, err
|
||||
}
|
||||
|
||||
tasks[r.ItemID].Reminders = append(tasks[r.ItemID].Reminders, date.In(config.GetTimeZone()))
|
||||
tasks[r.ItemID].Reminders = append(tasks[r.ItemID].Reminders, &models.TaskReminder{
|
||||
Reminder: date.In(config.GetTimeZone()),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return []*models.NamespaceWithProjectsAndTasks{
|
||||
|
|
|
@ -388,9 +388,9 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Done: false,
|
||||
Created: time1,
|
||||
Reminders: []time.Time{
|
||||
time.Date(2020, time.June, 15, 23, 59, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
Reminders: []*models.TaskReminder{
|
||||
{Reminder: time.Date(2020, time.June, 15, 23, 59, 0, 0, time.UTC).In(config.GetTimeZone())},
|
||||
{Reminder: time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -407,8 +407,8 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
Title: "Task400000002",
|
||||
Done: false,
|
||||
Created: time1,
|
||||
Reminders: []time.Time{
|
||||
time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
Reminders: []*models.TaskReminder{
|
||||
{Reminder: time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -421,8 +421,8 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
Created: time1,
|
||||
DoneAt: time3,
|
||||
Labels: vikunjaLabels,
|
||||
Reminders: []time.Time{
|
||||
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
Reminders: []*models.TaskReminder{
|
||||
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -441,8 +441,8 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
DueDate: dueTime,
|
||||
Created: time1,
|
||||
DoneAt: time3,
|
||||
Reminders: []time.Time{
|
||||
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
Reminders: []*models.TaskReminder{
|
||||
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -531,8 +531,8 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
Title: "Task400000009",
|
||||
Done: false,
|
||||
Created: time1,
|
||||
Reminders: []time.Time{
|
||||
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
Reminders: []*models.TaskReminder{
|
||||
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -141,7 +141,12 @@ func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) er
|
|||
comment.ID = 0
|
||||
}
|
||||
for _, attachment := range t.Attachments {
|
||||
af, err := storedFiles[attachment.File.ID].Open()
|
||||
attachmentFile, exists := storedFiles[attachment.File.ID]
|
||||
if !exists {
|
||||
log.Debugf(logPrefix+"Could not find attachment file %d for attachment %d", attachment.File.ID, attachment.ID)
|
||||
continue
|
||||
}
|
||||
af, err := attachmentFile.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open attachment %d for reading: %w", attachment.ID, err)
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -51,7 +51,7 @@ type VikunjaCaldavProjectStorage struct {
|
|||
}
|
||||
|
||||
// GetResources returns either all projects, links to the principal, or only one project, depending on the request
|
||||
func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren bool) ([]data.Resource, error) {
|
||||
func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, _ bool) ([]data.Resource, error) {
|
||||
|
||||
// It looks like we need to have the same handler for returning both the calendar home set and the user principal
|
||||
// Since the client seems to ignore the whatever is being returned in the first request and just makes a second one
|
||||
|
@ -166,7 +166,7 @@ func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) ([]
|
|||
}
|
||||
|
||||
// GetResourcesByFilters fetches a project of resources with a filter
|
||||
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByFilters(rpath string, filters *data.ResourceFilter) ([]data.Resource, error) {
|
||||
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByFilters(rpath string, _ *data.ResourceFilter) ([]data.Resource, error) {
|
||||
|
||||
// If we already have a project saved, that means the user is making a REPORT request to find out if
|
||||
// anything changed, in that case we need to return all tasks.
|
||||
|
@ -359,7 +359,7 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
|
|||
}
|
||||
|
||||
// DeleteResource deletes a resource
|
||||
func (vcls *VikunjaCaldavProjectStorage) DeleteResource(rpath string) error {
|
||||
func (vcls *VikunjaCaldavProjectStorage) DeleteResource(_ string) error {
|
||||
if vcls.task != nil {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
@ -411,13 +411,13 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
|
|||
return err
|
||||
}
|
||||
|
||||
labelMap := make(map[int64]*models.Label)
|
||||
labelMap := make(map[string]*models.Label)
|
||||
for _, l := range existingLabels {
|
||||
labelMap[l.ID] = &l.Label
|
||||
labelMap[l.Title] = &l.Label
|
||||
}
|
||||
|
||||
for _, label := range labels {
|
||||
if l, has := labelMap[label.ID]; has {
|
||||
if l, has := labelMap[label.Title]; has {
|
||||
*label = *l
|
||||
continue
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// Package swagger GENERATED BY SWAG; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag
|
||||
// Code generated by swaggo/swag. DO NOT EDIT
|
||||
package swagger
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
@ -7791,12 +7790,19 @@ const docTemplate = `{
|
|||
]
|
||||
},
|
||||
"reminder_dates": {
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.",
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"reminders": {
|
||||
"description": "An array of reminders that are associated with this task.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.TaskReminder"
|
||||
}
|
||||
},
|
||||
"repeat_after": {
|
||||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
|
@ -8303,6 +8309,19 @@ const docTemplate = `{
|
|||
"RelationKindCopiedTo"
|
||||
]
|
||||
},
|
||||
"models.ReminderRelation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"due_date",
|
||||
"start_date",
|
||||
"end_date"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ReminderRelationDueDate",
|
||||
"ReminderRelationStartDate",
|
||||
"ReminderRelationEndDate"
|
||||
]
|
||||
},
|
||||
"models.Right": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
@ -8518,12 +8537,19 @@ const docTemplate = `{
|
|||
]
|
||||
},
|
||||
"reminder_dates": {
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.",
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"reminders": {
|
||||
"description": "An array of reminders that are associated with this task.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.TaskReminder"
|
||||
}
|
||||
},
|
||||
"repeat_after": {
|
||||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
|
@ -8691,6 +8717,27 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.TaskReminder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"relative_period": {
|
||||
"description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date. Default: 0, tiggers when RelativeTo is due.",
|
||||
"type": "integer"
|
||||
},
|
||||
"relative_to": {
|
||||
"description": "The name of the date field to which the relative period refers to.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.ReminderRelation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reminder": {
|
||||
"description": "The absolute time when the user wants to be reminded of the task.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskRepeatMode": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
|
@ -7782,12 +7782,19 @@
|
|||
]
|
||||
},
|
||||
"reminder_dates": {
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.",
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"reminders": {
|
||||
"description": "An array of reminders that are associated with this task.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.TaskReminder"
|
||||
}
|
||||
},
|
||||
"repeat_after": {
|
||||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
|
@ -8294,6 +8301,19 @@
|
|||
"RelationKindCopiedTo"
|
||||
]
|
||||
},
|
||||
"models.ReminderRelation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"due_date",
|
||||
"start_date",
|
||||
"end_date"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ReminderRelationDueDate",
|
||||
"ReminderRelationStartDate",
|
||||
"ReminderRelationEndDate"
|
||||
]
|
||||
},
|
||||
"models.Right": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
@ -8509,12 +8529,19 @@
|
|||
]
|
||||
},
|
||||
"reminder_dates": {
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.",
|
||||
"description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"reminders": {
|
||||
"description": "An array of reminders that are associated with this task.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.TaskReminder"
|
||||
}
|
||||
},
|
||||
"repeat_after": {
|
||||
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
|
||||
"type": "integer"
|
||||
|
@ -8682,6 +8709,27 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.TaskReminder": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"relative_period": {
|
||||
"description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date. Default: 0, tiggers when RelativeTo is due.",
|
||||
"type": "integer"
|
||||
},
|
||||
"relative_to": {
|
||||
"description": "The name of the date field to which the relative period refers to.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.ReminderRelation"
|
||||
}
|
||||
]
|
||||
},
|
||||
"reminder": {
|
||||
"description": "The absolute time when the user wants to be reminded of the task.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.TaskRepeatMode": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
|
|
|
@ -196,11 +196,18 @@ definitions:
|
|||
- $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
|
||||
task.
|
||||
description: |-
|
||||
An array of datetimes when the user wants to be reminded of the task.
|
||||
|
||||
Deprecated: Use Reminders
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
reminders:
|
||||
description: An array of reminders that are associated with this task.
|
||||
items:
|
||||
$ref: '#/definitions/models.TaskReminder'
|
||||
type: array
|
||||
repeat_after:
|
||||
description: An amount in seconds this task repeats itself. If this is set,
|
||||
when marking the task as done, it will mark itself as "undone" and then
|
||||
|
@ -596,6 +603,16 @@ definitions:
|
|||
- RelationKindFollows
|
||||
- RelationKindCopiedFrom
|
||||
- RelationKindCopiedTo
|
||||
models.ReminderRelation:
|
||||
enum:
|
||||
- due_date
|
||||
- start_date
|
||||
- end_date
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- ReminderRelationDueDate
|
||||
- ReminderRelationStartDate
|
||||
- ReminderRelationEndDate
|
||||
models.Right:
|
||||
enum:
|
||||
- 0
|
||||
|
@ -763,11 +780,18 @@ definitions:
|
|||
- $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
|
||||
task.
|
||||
description: |-
|
||||
An array of datetimes when the user wants to be reminded of the task.
|
||||
|
||||
Deprecated: Use Reminders
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
reminders:
|
||||
description: An array of reminders that are associated with this task.
|
||||
items:
|
||||
$ref: '#/definitions/models.TaskReminder'
|
||||
type: array
|
||||
repeat_after:
|
||||
description: An amount in seconds this task repeats itself. If this is set,
|
||||
when marking the task as done, it will mark itself as "undone" and then
|
||||
|
@ -889,6 +913,22 @@ definitions:
|
|||
description: The ID of the "base" task, the task which has a relation to another.
|
||||
type: integer
|
||||
type: object
|
||||
models.TaskReminder:
|
||||
properties:
|
||||
relative_period:
|
||||
description: 'A period in seconds relative to another date argument. Negative
|
||||
values mean the reminder triggers before the date. Default: 0, tiggers when
|
||||
RelativeTo is due.'
|
||||
type: integer
|
||||
relative_to:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.ReminderRelation'
|
||||
description: The name of the date field to which the relative period refers
|
||||
to.
|
||||
reminder:
|
||||
description: The absolute time when the user wants to be reminded of the task.
|
||||
type: string
|
||||
type: object
|
||||
models.TaskRepeatMode:
|
||||
enum:
|
||||
- 0
|
||||
|
|
|
@ -39,7 +39,7 @@ func (s *IncreaseUserCounter) Name() string {
|
|||
return "increase.user.counter"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseUserCounter listens on is fired
|
||||
func (s *IncreaseUserCounter) Handle(msg *message.Message) (err error) {
|
||||
// Handle is executed when the event IncreaseUserCounter listens on is fired
|
||||
func (s *IncreaseUserCounter) Handle(_ *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.UserCountKey, 1)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// 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 utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParseISO8601Duration converts a ISO8601 duration into a time.Duration
|
||||
func ParseISO8601Duration(str string) time.Duration {
|
||||
matches := durationRegex.FindStringSubmatch(str)
|
||||
|
||||
if len(matches) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
years := parseDurationPart(matches[2], time.Hour*24*365)
|
||||
months := parseDurationPart(matches[3], time.Hour*24*30)
|
||||
days := parseDurationPart(matches[4], time.Hour*24)
|
||||
hours := parseDurationPart(matches[5], time.Hour)
|
||||
minutes := parseDurationPart(matches[6], time.Second*60)
|
||||
seconds := parseDurationPart(matches[7], time.Second)
|
||||
|
||||
duration := years + months + days + hours + minutes + seconds
|
||||
|
||||
if matches[1] == "-" {
|
||||
return -duration
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
var durationRegex = regexp.MustCompile(`([-+])?P([\d\.]+Y)?([\d\.]+M)?([\d\.]+D)?T?([\d\.]+H)?([\d\.]+M)?([\d\.]+?S)?`)
|
||||
|
||||
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
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// 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 utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseISO8601Duration(t *testing.T) {
|
||||
t.Run("full example", func(t *testing.T) {
|
||||
dur := ParseISO8601Duration("P1DT1H1M1S")
|
||||
expected, _ := time.ParseDuration("25h1m1s")
|
||||
|
||||
assert.Equal(t, expected, dur)
|
||||
})
|
||||
t.Run("negative duration", func(t *testing.T) {
|
||||
dur := ParseISO8601Duration("-P1DT1H1M1S")
|
||||
expected, _ := time.ParseDuration("-25h1m1s")
|
||||
|
||||
assert.Equal(t, expected, dur)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue