Compare commits

..

1 Commits

Author SHA1 Message Date
89347ab5fe fix(deps): update module github.com/spf13/cobra to v1.8.0 2023-11-04 22:02:44 +00:00
60 changed files with 1197 additions and 2959 deletions

View File

@ -191,10 +191,6 @@ ratelimit:
# Possible values are "keyvalue", "memory" or "redis".
# When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
store: keyvalue
# The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
# password confirmation, email verification, password reset request) per minute. This limit cannot be disabled.
# You should only change this if you know what you're doing.
noauthlimit: 10
files:
# The path where files are stored

View File

@ -19,7 +19,7 @@ To completely build Vikunja from source, you need to build the api and frontend.
The Vikunja API has no other dependencies than go itself.
That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.21`.
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.19`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. Clone the repo with `git clone https://code.vikunja.io/api` and switch into the directory.
4. Run `mage build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.

View File

@ -969,19 +969,6 @@ Full path: `ratelimit.store`
Environment path: `VIKUNJA_RATELIMIT_STORE`
### noauthlimit
The number of requests a user can make from the same IP to all unauthenticated routes (login, register,
password confirmation, email verification, password reset request) per minute. This limit cannot be disabled.
You should only change this if you know what you're doing.
Default: `10`
Full path: `ratelimit.noauthlimit`
Environment path: `VIKUNJA_RATELIMIT_NOAUTHLIMIT`
---
## files

View File

@ -88,27 +88,3 @@ Keycloak Config:
- Set `Root Url` to `https://vikunja.mydomain.com`
- Set `Valid redirect URIs` to `/auth/openid/keycloak`
- Create the client the navigate to the credentials tab and copy the `Client secret`
## Authentik
Authentik Config:
- Create a new Provider called "Vikunja" in Authentik
- Set the `Redirect URIs/Origins (RegEx)` to `https://vikunja.mydomain.com/auth/openid/authentik`
- Copy the Client ID and Client Secret
Vikunja Config:
```yaml
auth:
openid:
enabled: true
redirecturl: "https://vikunja.mydomain.com/auth/openid/"
providers:
- name: authentik
authurl: "https://authentik.mydomain.com/application/o/vikunja"
logouturl: "https://authentik.mydomain.com/application/o/vikunja/end-session/"
clientid: "" # copy from Authetik
clientsecret: "" # copy from Authentik
```
**Note:** The `authurl` that Vikunja requires is not the `Authorize URL` that you can see in the Provider. Vikunja uses Open ID Discovery to find the correct endpoint to use. Vikunja does this by automatically accessing the `OpenID Configuration URL` (usually `https://authentik.mydomain.com/application/o/vikunja/.well-known/openid-configuration`). Use this URL without the `.well-known/openid-configuration` as the `authurl`.

18
go.mod
View File

@ -47,12 +47,12 @@ require (
github.com/labstack/gommon v0.4.0
github.com/lib/pq v1.10.9
github.com/magefile/mage v1.15.0
github.com/mattn/go-sqlite3 v1.14.18
github.com/mattn/go-sqlite3 v1.14.17
github.com/olekukonko/tablewriter v0.0.5
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.17.0
github.com/redis/go-redis/v9 v9.3.0
github.com/redis/go-redis/v9 v9.2.1
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/spf13/afero v1.10.0
@ -64,18 +64,17 @@ require (
github.com/typesense/typesense-go v0.8.0
github.com/ulule/limiter/v3 v3.11.2
github.com/wneessen/go-mail v0.4.0
github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark v1.5.6
golang.org/x/crypto v0.14.0
golang.org/x/image v0.13.0
golang.org/x/oauth2 v0.13.0
golang.org/x/sync v0.5.0
golang.org/x/sys v0.14.0
golang.org/x/sync v0.4.0
golang.org/x/sys v0.13.0
golang.org/x/term v0.13.0
golang.org/x/text v0.13.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
src.techknowlogick.com/xgo v1.7.1-0.20231019133136-ecfba3dfed5d
src.techknowlogick.com/xormigrate v1.7.1
src.techknowlogick.com/xormigrate v1.7.0
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.4
)
@ -96,7 +95,7 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deepmap/oapi-codegen v1.13.4 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@ -106,7 +105,7 @@ require (
github.com/go-chi/chi/v5 v5.0.10 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
@ -174,6 +173,7 @@ require (
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect

36
go.sum
View File

@ -120,8 +120,8 @@ github.com/coreos/go-oidc/v3 v3.7.0/go.mod h1:yQzSCqBnK3e6Fs5l+f5i0F8Kwf0zpH9bPE
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cweill/gotests v1.6.0 h1:KJx+/p4EweijYzqPb4Y/8umDCip1Cv6hEVyOx0mE9W8=
@ -179,8 +179,8 @@ github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -464,8 +464,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI=
github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@ -525,8 +525,8 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@ -565,8 +565,8 @@ github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
@ -629,8 +629,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8=
@ -789,8 +789,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -850,8 +850,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -1117,8 +1117,8 @@ sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
src.techknowlogick.com/xgo v1.7.1-0.20231019133136-ecfba3dfed5d h1:3LCpRdOW7XJtEPpJC8yz4fSdZKKxX7o2/LqAD9vHKOw=
src.techknowlogick.com/xgo v1.7.1-0.20231019133136-ecfba3dfed5d/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=
src.techknowlogick.com/xormigrate v1.7.1/go.mod h1:YGNBdj8prENlySwIKmfoEXp7ILGjAltyKFXD0qLgD7U=
src.techknowlogick.com/xormigrate v1.7.0 h1:xLphJv5BbeeiVWNL817xiQXxJkKy9Q6ryp3gpLMZDWU=
src.techknowlogick.com/xormigrate v1.7.0/go.mod h1:YGNBdj8prENlySwIKmfoEXp7ILGjAltyKFXD0qLgD7U=
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=

View File

@ -38,21 +38,21 @@ type Todo struct {
UID string
// Optional
Summary string
Description string
Completed time.Time
Organizer *user.User
Priority int64 // 0-9, 1 is highest
Relations []Relation
Color string
Categories []string
Start time.Time
End time.Time
DueDate time.Time
Duration time.Duration
RepeatAfter int64
RepeatMode models.TaskRepeatMode
Alarms []Alarm
Summary string
Description string
Completed time.Time
Organizer *user.User
Priority int64 // 0-9, 1 is highest
RelatedToUID string
Color string
Categories []string
Start time.Time
End time.Time
DueDate time.Time
Duration time.Duration
RepeatAfter int64
RepeatMode models.TaskRepeatMode
Alarms []Alarm
Created time.Time
Updated time.Time // last-mod
@ -66,11 +66,6 @@ type Alarm struct {
Description string
}
type Relation struct {
Type models.RelationKind
UID string
}
// Config is the caldav calendar config
type Config struct {
Name string
@ -152,6 +147,11 @@ STATUS:COMPLETED`
ORGANIZER;CN=:` + t.Organizer.Username
}
if t.RelatedToUID != "" {
caldavtodos += `
RELATED-TO:` + t.RelatedToUID
}
if t.DueDate.Unix() > 0 {
caldavtodos += `
DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
@ -185,7 +185,6 @@ CATEGORIES:` + strings.Join(t.Categories, ",")
caldavtodos += `
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
caldavtodos += ParseAlarms(t.Alarms, t.Summary)
caldavtodos += ParseRelations(t.Relations)
caldavtodos += `
END:VTODO`
}
@ -223,47 +222,6 @@ END:VALARM`
return caldavalarms
}
func ParseRelations(relations []Relation) (caldavrelatedtos string) {
for _, r := range relations {
switch r.Type {
case models.RelationKindParenttask:
caldavrelatedtos += `
RELATED-TO;RELTYPE=PARENT:`
case models.RelationKindSubtask:
caldavrelatedtos += `
RELATED-TO;RELTYPE=CHILD:`
case models.RelationKindUnknown:
continue
case models.RelationKindRelated:
continue
case models.RelationKindDuplicateOf:
continue
case models.RelationKindDuplicates:
continue
case models.RelationKindBlocking:
continue
case models.RelationKindBlocked:
continue
case models.RelationKindPreceeds:
continue
case models.RelationKindFollows:
continue
case models.RelationKindCopiedFrom:
continue
case models.RelationKindCopiedTo:
continue
default:
caldavrelatedtos += `
RELATED-TO:`
}
caldavrelatedtos += r.UID
}
return caldavrelatedtos
}
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
return ts.In(time.UTC).Format(DateFormat) + "Z"
}

View File

@ -326,49 +326,6 @@ ACTION:DISPLAY
DESCRIPTION:Todo #1
END:VALARM
END:VTODO
END:VCALENDAR`,
},
{
name: "with related-to",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
},
todos: []*Todo{
{
Summary: "Todo #1",
Description: "Lorem Ipsum",
UID: "randommduid",
Relations: []Relation{
{
Type: models.RelationKindParenttask,
UID: "parentuid",
},
{
Type: models.RelationKindSubtask,
UID: "subtaskuid",
},
},
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
},
wantCaldavtasks: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204Z
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
LAST-MODIFIED:00010101T000000Z
RELATED-TO;RELTYPE=PARENT:parentuid
RELATED-TO;RELTYPE=CHILD:subtaskuid
END:VTODO
END:VCALENDAR`,
},
}

View File

@ -23,6 +23,7 @@ import (
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/utils"
@ -50,16 +51,6 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
})
}
var relations []Relation
for reltype, tasks := range t.RelatedTasks {
for _, r := range tasks {
relations = append(relations, Relation{
Type: reltype,
UID: r.UID,
})
}
}
caldavtodos = append(caldavtodos, &Todo{
Timestamp: t.Updated,
UID: t.UID,
@ -78,7 +69,6 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
RepeatAfter: t.RepeatAfter,
RepeatMode: t.RepeatMode,
Alarms: alarms,
Relations: relations,
})
}
@ -101,11 +91,11 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
}
// We put the vTodo details in a map to be able to handle them more easily
task := make(map[string]ics.IANAProperty)
var relations []ics.IANAProperty
var relation ics.IANAProperty
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
task[c.IANAToken] = c
if strings.HasPrefix(c.IANAToken, "RELATED-TO") {
relations = append(relations, c)
relation = c
}
}
@ -149,33 +139,17 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
}
for _, c := range relations {
var relTypeStr string
if _, ok := c.ICalParameters["RELTYPE"]; ok {
if len(c.ICalParameters["RELTYPE"]) != 1 {
continue
}
if relation.Value != "" {
s := db.NewSession()
defer s.Close()
relTypeStr = c.ICalParameters["RELTYPE"][0]
subtask, err := models.GetTaskSimpleByUUID(s, relation.Value)
if err != nil {
return nil, err
}
var relationKind models.RelationKind
switch relTypeStr {
case "PARENT":
relationKind = models.RelationKindParenttask
case "CHILD":
relationKind = models.RelationKindSubtask
default:
relationKind = models.RelationKindParenttask
}
if vTask.RelatedTasks == nil {
vTask.RelatedTasks = make(map[models.RelationKind][]*models.Task)
}
vTask.RelatedTasks[relationKind] = append(vTask.RelatedTasks[relationKind], &models.Task{
UID: c.Value,
})
vTask.RelatedTasks = make(map[models.RelationKind][]*models.Task)
vTask.RelatedTasks[models.RelationKindSubtask] = []*models.Task{subtask}
}
if task["STATUS"].Value == "COMPLETED" {

View File

@ -219,70 +219,6 @@ END:VCALENDAR`,
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
{
name: "With parent",
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:SubTask #1
DESCRIPTION:Lorem Ipsum
LAST-MODIFIED:00010101T000000
RELATED-TO;RELTYPE=PARENT:randomuid_parent
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "SubTask #1",
UID: "randomuid",
Description: "Lorem Ipsum",
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindParenttask: {
{
UID: "randomuid_parent",
},
},
},
},
},
{
name: "With subtask",
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:Parent
DESCRIPTION:Lorem Ipsum
LAST-MODIFIED:00010101T000000
RELATED-TO;RELTYPE=CHILD:randomuid_child
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "Parent",
UID: "randomuid",
Description: "Lorem Ipsum",
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
UID: "randomuid_child",
},
},
},
},
},
{
name: "example task from tasks.org app",
args: args{content: `BEGIN:VCALENDAR
@ -456,124 +392,6 @@ ACTION:DISPLAY
DESCRIPTION:Task 1
END:VALARM
END:VTODO
END:VCALENDAR`,
},
{
name: "Format Task with Related Tasks as CalDAV",
args: args{
list: &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: "List title",
},
},
tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Parent Task",
UID: "randomuid_parent",
Description: "A parent task",
Priority: 3,
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "Subtask 1",
UID: "randomuid_child_1",
Description: "The first child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
{
Title: "Subtask 2",
UID: "randomuid_child_2",
Description: "The second child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
},
},
},
{
Task: models.Task{
Title: "Subtask 1",
UID: "randomuid_child_1",
Description: "The first child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindParenttask: {
{
Title: "Parent task",
UID: "randomuid_parent",
Description: "A parent task",
Priority: 3,
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
},
},
},
},
},
{
Task: models.Task{
Title: "Subtask 2",
UID: "randomuid_child_2",
Description: "The second child task",
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindParenttask: {
{
Title: "Parent task",
UID: "randomuid_parent",
Description: "A parent task",
Priority: 3,
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
},
},
},
},
},
},
},
wantCaldav: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:List title
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:randomuid_parent
DTSTAMP:20181201T011205Z
SUMMARY:Parent Task
DESCRIPTION:A parent task
CREATED:20181201T011201Z
PRIORITY:3
LAST-MODIFIED:20181201T011205Z
RELATED-TO;RELTYPE=CHILD:randomuid_child_1
RELATED-TO;RELTYPE=CHILD:randomuid_child_2
END:VTODO
BEGIN:VTODO
UID:randomuid_child_1
DTSTAMP:20181201T011204Z
SUMMARY:Subtask 1
DESCRIPTION:The first child task
CREATED:20181201T011204Z
LAST-MODIFIED:20181201T011204Z
RELATED-TO;RELTYPE=PARENT:randomuid_parent
END:VTODO
BEGIN:VTODO
UID:randomuid_child_2
DTSTAMP:20181201T011204Z
SUMMARY:Subtask 2
DESCRIPTION:The second child task
CREATED:20181201T011204Z
LAST-MODIFIED:20181201T011204Z
RELATED-TO;RELTYPE=PARENT:randomuid_parent
END:VTODO
END:VCALENDAR`,
},
}

View File

@ -122,12 +122,11 @@ const (
LogMail Key = `log.mail`
LogMailLevel Key = `log.maillevel`
RateLimitEnabled Key = `ratelimit.enabled`
RateLimitKind Key = `ratelimit.kind`
RateLimitPeriod Key = `ratelimit.period`
RateLimitLimit Key = `ratelimit.limit`
RateLimitStore Key = `ratelimit.store`
RateLimitNoAuthRoutesLimit Key = `ratelimit.noauthlimit`
RateLimitEnabled Key = `ratelimit.enabled`
RateLimitKind Key = `ratelimit.kind`
RateLimitPeriod Key = `ratelimit.period`
RateLimitLimit Key = `ratelimit.limit`
RateLimitStore Key = `ratelimit.store`
FilesBasePath Key = `files.basepath`
FilesMaxSize Key = `files.maxsize`
@ -368,7 +367,6 @@ func InitDefaultConfig() {
RateLimitLimit.setDefault(100)
RateLimitPeriod.setDefault(60)
RateLimitStore.setDefault("memory")
RateLimitNoAuthRoutesLimit.setDefault(10)
// Files
FilesBasePath.setDefault("files")
FilesMaxSize.setDefault("20MB")

View File

@ -236,9 +236,3 @@
created_by_id: 15
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 39
title: testbucket38
project_id: 38
created_by_id: 15
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52

View File

@ -327,12 +327,3 @@
position: 1
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
id: 38
title: Project 38 for Caldav tests
description: Lorem Ipsum
identifier: test38
owner_id: 15
position: 2
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -34,39 +34,3 @@
relation_kind: 'related'
created_by_id: 1
created: 2018-12-01 15:13:12
- id: 7
task_id: 41
other_task_id: 43
relation_kind: 'subtask'
created_by_id: 15
created: 2018-12-01 15:13:12
- id: 8
task_id: 43
other_task_id: 41
relation_kind: 'parenttask'
created_by_id: 15
created: 2018-12-01 15:13:12
- id: 9
task_id: 41
other_task_id: 44
relation_kind: 'subtask'
created_by_id: 15
created: 2018-12-01 15:13:12
- id: 10
task_id: 44
other_task_id: 41
relation_kind: 'parenttask'
created_by_id: 15
created: 2018-12-01 15:13:12
- id: 11
task_id: 45
other_task_id: 46
relation_kind: 'subtask'
created_by_id: 15
created: 2018-12-01 15:13:12
- id: 12
task_id: 46
other_task_id: 45
relation_kind: 'parenttask'
created_by_id: 15
created: 2018-12-01 15:13:12

View File

@ -374,89 +374,5 @@
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 38
bucket_id: 1
position: 39
- id: 41
uid: 'uid-caldav-test-parent-task'
title: 'Parent task for Caldav Test'
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 36
index: 40
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 38
position: 40
- id: 42
uid: 'uid-caldav-test-parent-task-2'
title: 'Parent task for Caldav Test 2'
description: 'Description Caldav Test 2'
priority: 3
done: false
created_by_id: 15
project_id: 36
index: 41
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 38
position: 41
- id: 43
uid: 'uid-caldav-test-child-task'
title: 'Child task for Caldav Test'
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 36
index: 42
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 38
position: 42
- id: 44
uid: 'uid-caldav-test-child-task-2'
title: 'Child task for Caldav Test '
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 38
index: 43
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 38
position: 43
- id: 45
uid: 'uid-caldav-test-parent-task-another-list'
title: 'Parent task for Caldav Test'
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 36
index: 44
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 38
position: 44
- id: 46
uid: 'uid-caldav-test-child-task-another-list'
title: 'Child task for Caldav Test '
description: 'Description Caldav Test'
priority: 3
done: false
created_by_id: 15
project_id: 38
index: 45
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
bucket_id: 38
position: 45

View File

@ -100,15 +100,3 @@
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 18
user_id: 15
project_id: 36
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 19
user_id: 15
project_id: 38
right: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -23,8 +23,6 @@ import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/keyvalue"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
@ -79,8 +77,6 @@ func InitTests() {
}
InitTestFileHandler()
keyvalue.InitStorage()
}
// FileStat stats a file. This is an exported function to be able to test this from outide of the package

View File

@ -23,16 +23,14 @@ import (
"strconv"
"time"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/modules/keyvalue"
"code.vikunja.io/web"
"github.com/c2h5oh/datasize"
"github.com/spf13/afero"
"xorm.io/xorm"
)
// File holds all information about a file
@ -150,15 +148,10 @@ func (f *File) Delete() (err error) {
return err
}
return keyvalue.DecrBy(metrics.FilesCountKey, 1)
return
}
// Save saves a file to storage
func (f *File) Save(fcontent io.Reader) (err error) {
err = afs.WriteReader(f.getFileName(), fcontent)
if err != nil {
return
}
return keyvalue.IncrBy(metrics.FilesCountKey, 1)
func (f *File) Save(fcontent io.Reader) error {
return afs.WriteReader(f.getFileName(), fcontent)
}

View File

@ -29,7 +29,6 @@ import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/auth/openid"
"code.vikunja.io/api/pkg/modules/keyvalue"
migrationHandler "code.vikunja.io/api/pkg/modules/migration/handler"
"code.vikunja.io/api/pkg/red"
"code.vikunja.io/api/pkg/user"
)
@ -101,7 +100,6 @@ func FullInit() {
go func() {
models.RegisterListeners()
user.RegisterListeners()
migrationHandler.RegisterListeners()
err := events.InitEvents()
if err != nil {
log.Fatal(err.Error())

View File

@ -24,20 +24,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestCaldav(t *testing.T) {
t.Run("Delivers VTODO for project", func(t *testing.T) {
e, _ := setupTestEnv()
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:Project 36 for Caldav tests")
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
assert.Contains(t, rec.Body.String(), "END:VTODO")
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
})
t.Run("Import VTODO", func(t *testing.T) {
const vtodo = `BEGIN:VCALENDAR
const vtodo = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
@ -57,14 +44,24 @@ END:VALARM
END:VTODO
END:VCALENDAR`
e, _ := setupTestEnv()
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
func TestCaldav(t *testing.T) {
t.Run("Delivers VTODO for project", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:Project 36 for Caldav tests")
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
assert.Contains(t, rec.Body.String(), "END:VTODO")
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
})
t.Run("Import VTODO", func(t *testing.T) {
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
assert.NoError(t, err)
assert.Equal(t, 201, rec.Result().StatusCode)
})
t.Run("Export VTODO", func(t *testing.T) {
e, _ := setupTestEnv()
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test"})
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test")
@ -78,241 +75,3 @@ END:VCALENDAR`
assert.Contains(t, rec.Body.String(), "END:VALARM")
})
}
func TestCaldavSubtasks(t *testing.T) {
const vtodoHeader = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
`
const vtodoFooter = `
END:VCALENDAR`
t.Run("Import Task & Subtask", func(t *testing.T) {
const vtodoParentTaskStub = `BEGIN:VTODO
UID:uid_parent_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav parent task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO`
const vtodoChildTaskStub = `BEGIN:VTODO
UID:uid_child_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_import
END:VTODO`
const vtodoGrandChildTaskStub = `
BEGIN:VTODO
UID:uid_grand_child_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav grand child task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_child_import
END:VTODO`
e, _ := setupTestEnv()
const parentVTODO = vtodoHeader + vtodoParentTaskStub + vtodoFooter
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, parentVTODO, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
assert.NoError(t, err)
assert.Equal(t, 201, rec.Result().StatusCode)
const childVTODO = vtodoHeader + vtodoChildTaskStub + vtodoFooter
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, childVTODO, nil, map[string]string{"project": "36", "task": "uid_child_import"})
assert.NoError(t, err)
assert.Equal(t, 201, rec.Result().StatusCode)
const grandChildVTODO = vtodoHeader + vtodoGrandChildTaskStub + vtodoFooter
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, grandChildVTODO, nil, map[string]string{"project": "36", "task": "uid_grand_child_import"})
assert.NoError(t, err)
assert.Equal(t, 201, rec.Result().StatusCode)
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
assert.NoError(t, err)
assert.Equal(t, 200, rec.Result().StatusCode)
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_grand_child_import")
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child_import")
})
t.Run("Import Task & Subtask (Reverse - Subtask first)", func(t *testing.T) {
e, _ := setupTestEnv()
const vtodoGrandChildTaskStub = `
BEGIN:VTODO
UID:uid_grand_child_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav grand child task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_child_import
END:VTODO`
const grandChildVTODO = vtodoHeader + vtodoGrandChildTaskStub + vtodoFooter
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, grandChildVTODO, nil, map[string]string{"project": "36", "task": "uid_grand_child_import"})
assert.NoError(t, err)
assert.Equal(t, 201, rec.Result().StatusCode)
const vtodoChildTaskStub = `BEGIN:VTODO
UID:uid_child_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_import
RELATED-TO;RELTYPE=CHILD:uid_grand_child_import
END:VTODO`
const childVTODO = vtodoHeader + vtodoChildTaskStub + vtodoFooter
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, childVTODO, nil, map[string]string{"project": "36", "task": "uid_child_import"})
assert.NoError(t, err)
assert.Equal(t, 201, rec.Result().StatusCode)
const vtodoParentTaskStub = `BEGIN:VTODO
UID:uid_parent_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav parent task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=CHILD:uid_child_import
END:VTODO`
const parentVTODO = vtodoHeader + vtodoParentTaskStub + vtodoFooter
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, parentVTODO, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
assert.NoError(t, err)
assert.Equal(t, 201, rec.Result().StatusCode)
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
assert.NoError(t, err)
assert.Equal(t, 200, rec.Result().StatusCode)
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_grand_child_import")
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child_import")
})
t.Run("Delete Subtask", func(t *testing.T) {
e, _ := setupTestEnv()
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task"})
assert.NoError(t, err)
assert.Equal(t, 204, rec.Result().StatusCode)
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task-2"})
assert.NoError(t, err)
assert.Equal(t, 204, rec.Result().StatusCode)
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task"})
assert.NoError(t, err)
assert.Equal(t, 200, rec.Result().StatusCode)
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task")
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-2")
})
t.Run("Delete Parent Task", func(t *testing.T) {
e, _ := setupTestEnv()
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task"})
assert.NoError(t, err)
assert.Equal(t, 204, rec.Result().StatusCode)
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task"})
assert.NoError(t, err)
assert.Equal(t, 200, rec.Result().StatusCode)
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
})
}
func TestCaldavSubtasksDifferentLists(t *testing.T) {
t.Run("Import Parent Task & Child Task Different Lists", func(t *testing.T) {
const vtodoParentTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_parent_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav parent task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO
END:VCALENDAR`
const vtodoChildTask = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 38 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child_import
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_parent_import
END:VTODO
END:VCALENDAR`
e, _ := setupTestEnv()
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoParentTask, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask, nil, map[string]string{"project": "38", "task": "uid_child_import"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 201)
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "38", "task": "uid_child_import"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
})
t.Run("Check relationships across lists", func(t *testing.T) {
e, _ := setupTestEnv()
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task-another-list"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "UID:uid-caldav-test-parent-task-another-list")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-another-list")
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "38", "task": "uid-caldav-test-child-task-another-list"})
assert.NoError(t, err)
assert.Equal(t, rec.Result().StatusCode, 200)
assert.Contains(t, rec.Body.String(), "UID:uid-caldav-test-child-task-another-list")
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-another-list")
})
}

View File

@ -79,36 +79,23 @@ func setupTestEnv() (e *echo.Echo, err error) {
return
}
func createRequest(e *echo.Echo, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) {
func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values) (c echo.Context, rec *httptest.ResponseRecorder) {
// Setup
e, err := setupTestEnv()
assert.NoError(t, err)
// Do the actual request
req := httptest.NewRequest(method, "/", strings.NewReader(payload))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
req.URL.RawQuery = queryParam.Encode()
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)
var paramNames []string
var paramValues []string
for name, value := range urlParams {
paramNames = append(paramNames, name)
paramValues = append(paramValues, value)
}
c.SetParamNames(paramNames...)
c.SetParamValues(paramValues...)
return
}
func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) {
// Setup
e, err := setupTestEnv()
assert.NoError(t, err)
c, rec = createRequest(e, method, payload, queryParam, urlParams)
return
}
func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
var c echo.Context
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
err = handler(c)
return
}
@ -137,25 +124,36 @@ func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.
c.Set("user", tken)
}
func testRequestSetup(t *testing.T, method string, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, c echo.Context) {
c, rec = bootstrapTestRequest(t, method, payload, queryParams)
var paramNames []string
var paramValues []string
for name, value := range urlParams {
paramNames = append(paramNames, name)
paramValues = append(paramValues, value)
}
c.SetParamNames(paramNames...)
c.SetParamValues(paramValues...)
return
}
func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
var c echo.Context
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
addUserTokenToContext(t, user, c)
err = handler(c)
return
}
func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.HandlerFunc, share *models.LinkSharing, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
var c echo.Context
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
addLinkShareTokenToContext(t, share, c)
err = handler(c)
return
}
func newCaldavTestRequestWithUser(t *testing.T, e *echo.Echo, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
var c echo.Context
c, rec = createRequest(e, method, payload, queryParams, urlParams)
func newCaldavTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextPlain)
result, _ := caldav.BasicAuth(user.Username, "1234", c)

View File

@ -27,119 +27,83 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
)
const secondsUntilInactive = 30
const activeUsersKey = `active_users`
const activeLinkSharesKey = `active_link_shares`
// SecondsUntilInactive defines the seconds until a user is considered inactive
const SecondsUntilInactive = 30
// ActiveAuthenticable defines an active user or link share
type ActiveAuthenticable struct {
ID int64
// ActiveUsersKey is the key used to store active users in redis
const ActiveUsersKey = `activeusers`
// ActiveUser defines an active user
type ActiveUser struct {
UserID int64
LastSeen time.Time
}
type activeUsersMap map[int64]*ActiveAuthenticable
type activeUsersMap map[int64]*ActiveUser
// ActiveUsers is the type used to save active users
type ActiveUsers struct {
users activeUsersMap
mutex *sync.Mutex
}
// activeUsers holds a map with all active users
var activeUsers *ActiveUsers
type activeLinkSharesMap map[int64]*ActiveAuthenticable
type ActiveLinkShares struct {
shares activeLinkSharesMap
mutex *sync.Mutex
}
var activeLinkShares *ActiveLinkShares
func init() {
activeUsers = &ActiveUsers{
users: make(map[int64]*ActiveAuthenticable),
users: make(map[int64]*ActiveUser),
mutex: &sync.Mutex{},
}
activeLinkShares = &ActiveLinkShares{
shares: make(map[int64]*ActiveAuthenticable),
mutex: &sync.Mutex{},
}
}
func setupActiveUsersMetric() {
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_active_users",
Help: "The number of shares active within the last 30 seconds",
Help: "The number of users active within the last 30 seconds on this node",
}, func() float64 {
allActiveUsers := activeUsersMap{}
_, err := keyvalue.GetWithValue(activeUsersKey, &allActiveUsers)
allActiveUsers, err := getActiveUsers()
if err != nil {
log.Error(err.Error())
return 0
}
if allActiveUsers == nil {
return 0
}
count := 0
activeUsersCount := 0
for _, u := range allActiveUsers {
if time.Since(u.LastSeen) < secondsUntilInactive*time.Second {
count++
if time.Since(u.LastSeen) < SecondsUntilInactive*time.Second {
activeUsersCount++
}
}
return float64(count)
return float64(activeUsersCount)
}))
if err != nil {
log.Criticalf("Could not register metrics for currently active shares: %s", err)
log.Criticalf("Could not register metrics for currently active users: %s", err)
}
}
func setupActiveLinkSharesMetric() {
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_active_link_shares",
Help: "The number of link shares active within the last 30 seconds. Similar to vikunja_active_users.",
}, func() float64 {
allActiveLinkShares := activeLinkSharesMap{}
_, err := keyvalue.GetWithValue(activeLinkSharesKey, &allActiveLinkShares)
if err != nil {
log.Error(err.Error())
return 0
}
if allActiveLinkShares == nil {
return 0
}
count := 0
for _, u := range allActiveLinkShares {
if time.Since(u.LastSeen) < secondsUntilInactive*time.Second {
count++
}
}
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for currently active link shares: %s", err)
}
}
// SetUserActive sets a user as active and pushes it to keyvalue
// SetUserActive sets a user as active and pushes it to redis
func SetUserActive(a web.Auth) (err error) {
activeUsers.mutex.Lock()
activeUsers.users[a.GetID()] = &ActiveUser{
UserID: a.GetID(),
LastSeen: time.Now(),
}
activeUsers.mutex.Unlock()
return PushActiveUsers()
}
// getActiveUsers returns the active users from redis
func getActiveUsers() (users activeUsersMap, err error) {
users = activeUsersMap{}
_, err = keyvalue.GetWithValue(ActiveUsersKey, &users)
return
}
// PushActiveUsers pushed the content of the activeUsers map to redis
func PushActiveUsers() (err error) {
activeUsers.mutex.Lock()
defer activeUsers.mutex.Unlock()
activeUsers.users[a.GetID()] = &ActiveAuthenticable{
ID: a.GetID(),
LastSeen: time.Now(),
}
return keyvalue.Put(activeUsersKey, activeUsers.users)
}
// SetLinkShareActive sets a user as active and pushes it to keyvalue
func SetLinkShareActive(a web.Auth) (err error) {
activeLinkShares.mutex.Lock()
defer activeLinkShares.mutex.Unlock()
activeLinkShares.shares[a.GetID()] = &ActiveAuthenticable{
ID: a.GetID(),
LastSeen: time.Now(),
}
return keyvalue.Put(activeLinkSharesKey, activeLinkShares.shares)
return keyvalue.Put(ActiveUsersKey, activeUsers.users)
}

View File

@ -28,12 +28,17 @@ import (
)
const (
ProjectCountKey = `project_count`
UserCountKey = `user_count`
TaskCountKey = `task_count`
TeamCountKey = `team_count`
FilesCountKey = `files_count`
AttachmentsCountKey = `attachments_count`
// ProjectCountKey is the name of the key in which we save the project count
ProjectCountKey = `projectcount`
// UserCountKey is the name of the key we use to store total users in redis
UserCountKey = `usercount`
// TaskCountKey is the name of the key we use to store the amount of total tasks in redis
TaskCountKey = `taskcount`
// TeamCountKey is the name of the key we use to store the amount of total teams in redis
TeamCountKey = `teamcount`
)
var registry *prometheus.Registry
@ -48,32 +53,64 @@ func GetRegistry() *prometheus.Registry {
return registry
}
func registerPromMetric(key, description string) {
// InitMetrics Initializes the metrics
func InitMetrics() {
// init active users, sometimes we'll have garbage from previous runs in redis instead
if err := PushActiveUsers(); err != nil {
log.Fatalf("Could not set initial count for active users, error was %s", err)
}
GetRegistry()
// Register total project count metric
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_" + key,
Help: description,
Name: "vikunja_project_count",
Help: "The number of projects on this instance",
}, func() float64 {
count, _ := GetCount(key)
count, _ := GetCount(ProjectCountKey)
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", key, err)
log.Criticalf("Could not register metrics for %s: %s", ProjectCountKey, err)
}
}
// InitMetrics Initializes the metrics
func InitMetrics() {
GetRegistry()
// Register total user count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_user_count",
Help: "The total number of users on this instance",
}, func() float64 {
count, _ := GetCount(UserCountKey)
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", UserCountKey, err)
}
registerPromMetric(ProjectCountKey, "The number of projects on this instance")
registerPromMetric(UserCountKey, "The total number of shares on this instance")
registerPromMetric(TaskCountKey, "The total number of tasks on this instance")
registerPromMetric(TeamCountKey, "The total number of teams on this instance")
registerPromMetric(FilesCountKey, "The total number of files on this instance")
registerPromMetric(AttachmentsCountKey, "The total number of attachments on this instance")
// Register total Tasks count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_task_count",
Help: "The total number of tasks on this instance",
}, func() float64 {
count, _ := GetCount(TaskCountKey)
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", TaskCountKey, err)
}
// Register total teams count metric
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
Name: "vikunja_team_count",
Help: "The total number of teams on this instance",
}, func() float64 {
count, _ := GetCount(TeamCountKey)
return float64(count)
}))
if err != nil {
log.Criticalf("Could not register metrics for %s: %s", TeamCountKey, err)
}
setupActiveUsersMetric()
setupActiveLinkSharesMetric()
}
// GetCount returns the current count from keyvalue

View File

@ -1,97 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"time"
"code.vikunja.io/api/pkg/config"
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type migrationStatus20231108231513 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
Created time.Time `xorm:"created not null 'created'" json:"time"`
StartedAt time.Time `xorm:"null" json:"started_at"`
FinishedAt time.Time `xorm:"null" json:"finished_at"`
}
func (migrationStatus20231108231513) TableName() string {
return "migration_status"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20231108231513",
Description: "",
Migrate: func(tx *xorm.Engine) error {
err := tx.Sync2(migrationStatus20231108231513{})
if err != nil {
return err
}
all := []*migrationStatus20231108231513{}
err = tx.Find(&all)
if err != nil {
return err
}
for _, status := range all {
status.StartedAt = status.Created
status.FinishedAt = status.Created
_, err = tx.Where("id = ?", status.ID).Update(status)
if err != nil {
return err
}
}
if config.DatabaseType.GetString() == "sqlite" {
_, err = tx.Exec(`create table migration_status_dg_tmp
(
id INTEGER not null
primary key autoincrement,
user_id INTEGER not null,
migrator_name TEXT,
started_at DATETIME null,
finished_at DATETIME null
);
insert into migration_status_dg_tmp(id, user_id, migrator_name, started_at, finished_at)
select id, user_id, migrator_name, started_at, finished_at
from migration_status;
drop table migration_status;
alter table migration_status_dg_tmp
rename to migration_status;
create unique index UQE_migration_status_id
on migration_status (id);
`)
return err
}
err = dropTableColum(tx, "migration_status", "created")
return err
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -74,6 +74,7 @@ func CollectRoutesForAPITokenUsage(route echo.Route) {
routeGroupName := getRouteGroupName(route.Path)
if routeGroupName == "subscriptions" ||
routeGroupName == "notifications" ||
routeGroupName == "tokens" ||
strings.HasSuffix(routeGroupName, "_bulk") {
return

View File

@ -161,7 +161,7 @@ func IsErrNeedToHaveProjectReadAccess(err error) bool {
}
func (err ErrNeedToHaveProjectReadAccess) Error() string {
return fmt.Sprintf("User needs to have read access to that project [ProjectID: %d, ID: %d]", err.ProjectID, err.UserID)
return fmt.Sprintf("User needs to have read access to that project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID)
}
// ErrCodeNeedToHaveProjectReadAccess holds the unique world-error code of this error
@ -518,7 +518,7 @@ func IsErrNoRightToSeeTask(err error) bool {
}
func (err ErrNoRightToSeeTask) Error() string {
return fmt.Sprintf("User does not have the right to see the task [TaskID: %v, ID: %v]", err.TaskID, err.UserID)
return fmt.Sprintf("User does not have the right to see the task [TaskID: %v, UserID: %v]", err.TaskID, err.UserID)
}
// ErrCodeNoRightToSeeTask holds the unique world-error code of this error
@ -961,7 +961,7 @@ func IsErrUserAlreadyAssigned(err error) bool {
}
func (err ErrUserAlreadyAssigned) Error() string {
return fmt.Sprintf("User is already assigned to task [TaskID: %d, ID: %d]", err.TaskID, err.UserID)
return fmt.Sprintf("User is already assigned to task [TaskID: %d, UserID: %d]", err.TaskID, err.UserID)
}
// ErrCodeUserAlreadyAssigned holds the unique world-error code of this error
@ -1190,7 +1190,7 @@ func IsErrUserDoesNotHaveAccessToProject(err error) bool {
}
func (err ErrUserDoesNotHaveAccessToProject) Error() string {
return fmt.Sprintf("User does not have access to the project [ProjectID: %d, ID: %d]", err.ProjectID, err.UserID)
return fmt.Sprintf("User does not have access to the project [ProjectID: %d, UserID: %d]", err.ProjectID, err.UserID)
}
// ErrCodeUserDoesNotHaveAccessToProject holds the unique world-error code of this error
@ -1273,7 +1273,7 @@ func IsErrUserHasNoAccessToLabel(err error) bool {
}
func (err ErrUserHasNoAccessToLabel) Error() string {
return fmt.Sprintf("The user does not have access to this label [LabelID: %v, ID: %v]", err.LabelID, err.UserID)
return fmt.Sprintf("The user does not have access to this label [LabelID: %v, UserID: %v]", err.LabelID, err.UserID)
}
// ErrCodeUserHasNoAccessToLabel holds the unique world-error code of this error
@ -1568,7 +1568,7 @@ func IsErrSubscriptionAlreadyExists(err error) bool {
}
func (err *ErrSubscriptionAlreadyExists) Error() string {
return fmt.Sprintf("Subscription for this (entity_id, entity_type, user_id) already exists [EntityType: %d, EntityID: %d, ID: %d]", err.EntityType, err.EntityID, err.UserID)
return fmt.Sprintf("Subscription for this (entity_id, entity_type, user_id) already exists [EntityType: %d, EntityID: %d, UserID: %d]", err.EntityType, err.EntityID, err.UserID)
}
// ErrCodeSubscriptionAlreadyExists holds the unique world-error code of this error

View File

@ -147,7 +147,7 @@ func TestBucket_Delete(t *testing.T) {
tasks := []*Task{}
err = s.Where("bucket_id = ?", 1).Find(&tasks)
assert.NoError(t, err)
assert.Len(t, tasks, 15)
assert.Len(t, tasks, 16)
db.AssertMissing(t, "buckets", map[string]interface{}{
"id": 2,
"project_id": 1,

View File

@ -176,23 +176,11 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
cond = builder.And(builder.In("label_tasks.task_id", opts.TaskIDs), cond)
}
if opts.GetForUser != 0 {
projects, _, _, err := getRawProjectsForUser(s, &projectOptions{
user: &user.User{ID: opts.GetForUser},
})
if err != nil {
return nil, 0, 0, err
}
projectIDs := make([]int64, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
cond = builder.And(builder.In("label_tasks.task_id",
builder.
Select("id").
From("tasks").
Where(builder.In("project_id", projectIDs)),
Where(builder.In("project_id", getUserProjectsStatement(nil, opts.GetForUser, "", false).Select("l.id"))),
), cond)
}
if opts.GetUnusedLabels {

View File

@ -37,16 +37,12 @@ import (
// RegisterListeners registers all event listeners
func RegisterListeners() {
if config.MetricsEnabled.GetBool() {
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &IncreaseProjectCounter{})
events.RegisterListener((&ProjectDeletedEvent{}).Name(), &DecreaseProjectCounter{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
events.RegisterListener((&TeamCreatedEvent{}).Name(), &IncreaseTeamCounter{})
events.RegisterListener((&TaskAttachmentCreatedEvent{}).Name(), &IncreaseAttachmentCounter{})
events.RegisterListener((&TaskAttachmentDeletedEvent{}).Name(), &DecreaseAttachmentCounter{})
}
events.RegisterListener((&ProjectCreatedEvent{}).Name(), &IncreaseProjectCounter{})
events.RegisterListener((&ProjectDeletedEvent{}).Name(), &DecreaseProjectCounter{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &IncreaseTaskCounter{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &DecreaseTaskCounter{})
events.RegisterListener((&TeamDeletedEvent{}).Name(), &DecreaseTeamCounter{})
events.RegisterListener((&TeamCreatedEvent{}).Name(), &IncreaseTeamCounter{})
events.RegisterListener((&TaskCommentCreatedEvent{}).Name(), &SendTaskCommentNotification{})
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SendTaskAssignedNotification{})
events.RegisterListener((&TaskDeletedEvent{}).Name(), &SendTaskDeletedNotification{})
@ -562,34 +558,6 @@ func (l *AddTaskToTypesense) Handle(msg *message.Message) (err error) {
return
}
// IncreaseAttachmentCounter represents a listener
type IncreaseAttachmentCounter struct {
}
// Name defines the name for the IncreaseAttachmentCounter listener
func (s *IncreaseAttachmentCounter) Name() string {
return "increase.attachment.counter"
}
// Handle is executed when the event IncreaseAttachmentCounter listens on is fired
func (s *IncreaseAttachmentCounter) Handle(_ *message.Message) (err error) {
return keyvalue.IncrBy(metrics.AttachmentsCountKey, 1)
}
// DecreaseAttachmentCounter represents a listener
type DecreaseAttachmentCounter struct {
}
// Name defines the name for the DecreaseAttachmentCounter listener
func (s *DecreaseAttachmentCounter) Name() string {
return "decrease.attachment.counter"
}
// Handle is executed when the event DecreaseAttachmentCounter listens on is fired
func (s *DecreaseAttachmentCounter) Handle(_ *message.Message) (err error) {
return keyvalue.DecrBy(metrics.AttachmentsCountKey, 1)
}
///////
// Project Event Listeners

View File

@ -416,13 +416,12 @@ func getUserProjectsStatement(parentProjectIDs []int64, userID int64, search str
filterCond,
getArchivedCond,
parentCondition,
builder.NotIn("l.id", parentProjectIDs),
)).
OrderBy("position").
GroupBy("l.id")
}
func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int64, opts *projectOptions, projects *[]*Project, oldTotalCount int64, archivedProjects map[int64]bool) (resultCount int, totalCount int64, err error) {
func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int64, opts *projectOptions, projects *[]*Project, oldTotalCount int64) (resultCount int, totalCount int64, err error) {
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
query := getUserProjectsStatement(parentProjectIDs, userID, opts.search, opts.getArchived)
@ -455,12 +454,6 @@ func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int
newParentIDs := []int64{}
for _, project := range currentProjects {
if project.IsArchived {
archivedProjects[project.ID] = true
}
if archivedProjects[project.ParentProjectID] {
project.IsArchived = true
}
// Filter out parent project ids which we're not looking for to avoid leaking
// information about parent projects
if !parentIDsMap[project.ParentProjectID] {
@ -474,7 +467,7 @@ func getAllProjectsForUser(s *xorm.Session, userID int64, parentProjectIDs []int
// If we don't reset the limit for subprojects, it will be impossible to fetch all subprojects.
opts.page = -1
return getAllProjectsForUser(s, userID, newParentIDs, opts, projects, oldTotalCount+totalCount, archivedProjects)
return getAllProjectsForUser(s, userID, newParentIDs, opts, projects, oldTotalCount+totalCount)
}
// Gets the projects with their children without any tasks
@ -485,8 +478,7 @@ func getRawProjectsForUser(s *xorm.Session, opts *projectOptions) (projects []*P
}
allProjects := []*Project{}
archivedProjects := make(map[int64]bool)
resultCount, totalItems, err = getAllProjectsForUser(s, fullUser.ID, nil, opts, &allProjects, 0, archivedProjects)
resultCount, totalItems, err = getAllProjectsForUser(s, fullUser.ID, nil, opts, &allProjects, 0)
if err != nil {
return
}
@ -1041,28 +1033,10 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
return
}
err = events.Dispatch(&ProjectDeletedEvent{
return events.Dispatch(&ProjectDeletedEvent{
Project: fullProject,
Doer: a,
})
if err != nil {
return
}
childProjects := []*Project{}
err = s.Where("parent_project_id = ?", fullProject.ID).Find(&childProjects)
if err != nil {
return
}
for _, child := range childProjects {
err = child.Delete(s, a)
if err != nil {
return
}
}
return
}
// DeleteBackgroundFileIfExists deletes the list's background file from the db and the filesystem,

View File

@ -348,10 +348,9 @@ func TestProject_ReadAll(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
projects := []*Project{}
archivedProjects := make(map[int64]bool)
_, _, err := getAllProjectsForUser(s, 1, nil, &projectOptions{}, &projects, 0, archivedProjects)
_, _, err := getAllProjectsForUser(s, 1, nil, &projectOptions{}, &projects, 0)
assert.NoError(t, err)
assert.Equal(t, 24, len(projects))
assert.Equal(t, 25, len(projects))
_ = s.Close()
})
t.Run("only child projects for one project", func(t *testing.T) {
@ -367,12 +366,12 @@ func TestProject_ReadAll(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(projects3).Kind(), reflect.Slice)
ls := projects3.([]*Project)
assert.Equal(t, 26, len(ls))
assert.Equal(t, 27, len(ls))
assert.Equal(t, int64(3), ls[0].ID) // Project 3 has a position of 1 and should be sorted first
assert.Equal(t, int64(1), ls[1].ID)
assert.Equal(t, int64(6), ls[2].ID)
assert.Equal(t, int64(-1), ls[24].ID)
assert.Equal(t, int64(-2), ls[25].ID)
assert.Equal(t, int64(-1), ls[25].ID)
assert.Equal(t, int64(-2), ls[26].ID)
_ = s.Close()
})
t.Run("projects for nonexistant user", func(t *testing.T) {

View File

@ -124,7 +124,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
// @tags task
// @Accept json
// @Produce json
// @Param id path int true "The project ID."
// @Param projectID path int true "The project ID."
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
@ -138,7 +138,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
// @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/tasks [get]
// @Router /projects/{projectID}/tasks [get]
func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// If the project id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter

View File

@ -19,7 +19,6 @@ package models
import (
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
@ -297,8 +296,6 @@ func convertFilterValues(value interface{}) string {
}
return "false"
case time.Time:
return strconv.FormatInt(v.Unix(), 10)
}
log.Errorf("Unknown search type for value %v", value)

View File

@ -373,14 +373,7 @@ func (bt *BulkTask) GetTasksByIDs(s *xorm.Session) (err error) {
}
func GetTaskSimpleByUUID(s *xorm.Session, uid string) (task *Task, err error) {
var has bool
task = &Task{}
has, err = s.In("uid", uid).Get(task)
if !has || err != nil {
return &Task{}, ErrTaskDoesNotExist{}
}
_, err = s.In("uid", uid).Get(task)
return
}
@ -825,13 +818,13 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "The Task ID"
// @Param ID path int true "The Task ID"
// @Param task body models.Task true "The task object"
// @Success 200 {object} models.Task "The updated task object."
// @Failure 400 {object} web.HTTPError "Invalid task object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its project)"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id} [post]
// @Router /tasks/{ID} [post]
//
//nolint:gocyclo
func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
@ -1417,12 +1410,12 @@ func updateTaskLastUpdated(s *xorm.Session, task *Task) error {
// @tags task
// @Produce json
// @Security JWTKeyAuth
// @Param id path int true "Task ID"
// @Param ID path int true "Task ID"
// @Success 200 {object} models.Message "The created task object."
// @Failure 400 {object} web.HTTPError "Invalid task ID provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id} [delete]
// @Router /tasks/{ID} [delete]
func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
// duplicate the task for the event

View File

@ -137,10 +137,6 @@ func DeleteUser(s *xorm.Session, u *user.User) (err error) {
}
for _, p := range projectsToDelete {
if p.ParentProjectID != 0 {
// Child projects are deleted by p.Delete
continue
}
err = p.Delete(s, u)
// If the user is the owner of the default project it will be deleted, if they are not the owner
// we can ignore the error as the project was shared in that case.

View File

@ -285,7 +285,6 @@ func (w *Webhook) sendWebhookPayload(p *WebhookPayload) (err error) {
}
req.Header.Add("User-Agent", "Vikunja/"+version.Version)
req.Header.Add("Content-Type", "application/json")
client := getWebhookHTTPClient()
res, err := client.Do(req)

View File

@ -21,7 +21,6 @@ import (
"encoding/json"
"errors"
"net/http"
"strings"
"code.vikunja.io/web/handler"
@ -217,7 +216,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
// If no user exists, create one with the preferred username if it is not already taken
if user.IsErrUserDoesNotExist(err) {
uu := &user.User{
Username: strings.ReplaceAll(cl.PreferredUsername, " ", "-"),
Username: cl.PreferredUsername,
Email: cl.Email,
Name: cl.Name,
Status: user.StatusActive,
@ -235,7 +234,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
return nil, err
}
// If their preferred username is already taken, generate a random one
// If their preferred username is already taken, create some random one from the email and subject
if user.IsErrUsernameExists(err) {
uu.Username = petname.Generate(3, "-")
u, err = user.CreateUser(s, uu)

View File

@ -60,7 +60,6 @@ func insertFromStructure(s *xorm.Session, str []*models.ProjectWithTasksAndBucke
if p.ParentProjectID != 0 {
childRelations[p.ParentProjectID] = append(childRelations[p.ParentProjectID], oldID)
p.ParentProjectID = 0
}
p.ID = 0

View File

@ -1,33 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 handler
import (
"code.vikunja.io/api/pkg/user"
)
// MigrationRequestedEvent represents a MigrationRequestedEvent event
type MigrationRequestedEvent struct {
Migrator interface{} `json:"migrator"`
User *user.User `json:"user"`
MigratorKind string `json:"migrator_kind"`
}
// Name defines the name for MigrationRequestedEvent
func (t *MigrationRequestedEvent) Name() string {
return "migration.requested"
}

View File

@ -19,7 +19,6 @@ package handler
import (
"net/http"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
user2 "code.vikunja.io/api/pkg/user"
@ -27,12 +26,6 @@ import (
"github.com/labstack/echo/v4"
)
var registeredMigrators map[string]*MigrationWeb
func init() {
registeredMigrators = make(map[string]*MigrationWeb)
}
// MigrationWeb holds the web migration handler
type MigrationWeb struct {
MigrationStruct func() migration.Migrator
@ -43,13 +36,12 @@ type AuthURL struct {
URL string `json:"url"`
}
// RegisterMigrator registers all routes for migration
func (mw *MigrationWeb) RegisterMigrator(g *echo.Group) {
// RegisterRoutes registers all routes for migration
func (mw *MigrationWeb) RegisterRoutes(g *echo.Group) {
ms := mw.MigrationStruct()
g.GET("/"+ms.Name()+"/auth", mw.AuthURL)
g.GET("/"+ms.Name()+"/status", mw.Status)
g.POST("/"+ms.Name()+"/migrate", mw.Migrate)
registeredMigrators[ms.Name()] = mw
}
// AuthURL is the web handler to get the auth url
@ -68,29 +60,19 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
stats, err := migration.GetMigrationStatus(ms, user)
if err != nil {
return handler.HandleHTTPError(err, c)
}
if stats.FinishedAt.IsZero() {
return c.JSON(http.StatusOK, map[string]string{
"message": "Migration already running",
"running_since": stats.StartedAt.String(),
})
}
// Bind user request stuff
err = c.Bind(ms)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error())
}
err = events.Dispatch(&MigrationRequestedEvent{
Migrator: ms,
MigratorKind: ms.Name(),
User: user,
})
// Do the migration
err = ms.Migrate(user)
if err != nil {
return handler.HandleHTTPError(err, c)
}
err = migration.SetMigrationStatus(ms, user)
if err != nil {
return handler.HandleHTTPError(err, c)
}

View File

@ -57,18 +57,13 @@ func (fw *FileMigratorWeb) Migrate(c echo.Context) error {
}
defer src.Close()
m, err := migration.StartMigration(ms, user)
if err != nil {
return handler.HandleHTTPError(err, c)
}
// Do the migration
err = ms.Migrate(user, src, file.Size)
if err != nil {
return handler.HandleHTTPError(err, c)
}
err = migration.FinishMigration(m)
err = migration.SetMigrationStatus(ms, user)
if err != nil {
return handler.HandleHTTPError(err, c)
}

View File

@ -1,88 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 handler
import (
"encoding/json"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/notifications"
"github.com/ThreeDotsLabs/watermill/message"
)
func RegisterListeners() {
events.RegisterListener((&MigrationRequestedEvent{}).Name(), &MigrationListener{})
}
// MigrationListener represents a listener
type MigrationListener struct {
}
// Name defines the name for the MigrationListener listener
func (s *MigrationListener) Name() string {
return "migration.listener"
}
// Handle is executed when the event MigrationListener listens on is fired
func (s *MigrationListener) Handle(msg *message.Message) (err error) {
event := &MigrationRequestedEvent{}
err = json.Unmarshal(msg.Payload, event)
if err != nil {
return
}
mstr := registeredMigrators[event.MigratorKind]
event.Migrator = mstr.MigrationStruct()
// unmarshaling again to make sure the migrator has the correct type now
err = json.Unmarshal(msg.Payload, event)
if err != nil {
return
}
ms := event.Migrator.(migration.Migrator)
m, err := migration.StartMigration(ms, event.User)
if err != nil {
return
}
log.Debugf("[Migration] Starting migration %d from %s for user %d", m.ID, event.MigratorKind, event.User.ID)
err = ms.Migrate(event.User)
if err != nil {
return
}
err = migration.FinishMigration(m)
if err != nil {
return
}
err = notifications.Notify(event.User, &MigrationDoneNotification{
MigratorName: ms.Name(),
})
if err != nil {
return
}
log.Debugf("[Migration] Successfully done migration %d from %s for user %d", m.ID, event.MigratorKind, event.User.ID)
return
}

View File

@ -1,51 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 handler
import (
"golang.org/x/text/cases"
"golang.org/x/text/language"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/notifications"
)
// MigrationDoneNotification represents a MigrationDoneNotification notification
type MigrationDoneNotification struct {
MigratorName string
}
// ToMail returns the mail notification for MigrationDoneNotification
func (n *MigrationDoneNotification) ToMail() *notifications.Mail {
kind := cases.Title(language.English).String(n.MigratorName)
return notifications.NewMail().
Subject("The migration from "+kind+" to Vikunja was completed").
Line("Vikunja has imported all lists/projects, tasks, notes, reminders and files from "+kind+" you have access to.").
Action("View your imported projects in Vikunja", config.ServiceFrontendurl.GetString()).
Line("Have fun with your new (old) projects!")
}
// ToDB returns the MigrationDoneNotification notification in a format which can be saved in the db
func (n *MigrationDoneNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *MigrationDoneNotification) Name() string {
return "migration.done"
}

View File

@ -261,30 +261,26 @@ func getMicrosoftTodoData(token string) (microsoftTodoData []*project, err error
func convertMicrosoftTodoData(todoData []*project) (vikunjsStructure []*models.ProjectWithTasksAndBuckets, err error) {
var pseudoParentID int64 = 1
// One project with all child projects
vikunjsStructure = []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
ID: pseudoParentID,
Title: "Migrated from Microsoft Todo",
},
ChildProjects: []*models.ProjectWithTasksAndBuckets{},
},
}
log.Debugf("[Microsoft Todo Migration] Converting %d projects", len(todoData))
for index, l := range todoData {
for _, l := range todoData {
log.Debugf("[Microsoft Todo Migration] Converting project %s", l.ID)
// Projects only with title
project := &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: l.DisplayName,
ID: int64(index+1) + pseudoParentID,
ParentProjectID: pseudoParentID,
Title: l.DisplayName,
},
}
@ -368,7 +364,7 @@ func convertMicrosoftTodoData(todoData []*project) (vikunjsStructure []*models.P
log.Debugf("[Microsoft Todo Migration] Done converted %d tasks", len(l.Tasks))
}
vikunjsStructure = append(vikunjsStructure, project)
vikunjsStructure[0].ChildProjects = append(vikunjsStructure[0].ChildProjects, project)
log.Debugf("[Microsoft Todo Migration] Done converting project %s", l.ID)
}

View File

@ -106,81 +106,78 @@ func TestConverting(t *testing.T) {
{
Project: models.Project{
Title: "Migrated from Microsoft Todo",
ID: 1,
},
},
{
Project: models.Project{
ID: 2,
ParentProjectID: 1,
Title: "Project 1",
},
Tasks: []*models.TaskWithComments{
ChildProjects: []*models.ProjectWithTasksAndBuckets{
{
Task: models.Task{
Title: "Task 1",
Description: "This is a description",
Project: models.Project{
Title: "Project 1",
},
},
{
Task: models.Task{
Title: "Task 2",
Done: true,
DoneAt: testtimeTime,
},
},
{
Task: models.Task{
Title: "Task 3",
Priority: 1,
},
},
{
Task: models.Task{
Title: "Task 4",
Priority: 3,
},
},
{
Task: models.Task{
Title: "Task 5",
Reminders: []*models.TaskReminder{
{
Reminder: testtimeTime,
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task 1",
Description: "This is a description",
},
},
{
Task: models.Task{
Title: "Task 2",
Done: true,
DoneAt: testtimeTime,
},
},
{
Task: models.Task{
Title: "Task 3",
Priority: 1,
},
},
{
Task: models.Task{
Title: "Task 4",
Priority: 3,
},
},
{
Task: models.Task{
Title: "Task 5",
Reminders: []*models.TaskReminder{
{
Reminder: testtimeTime,
},
},
},
},
{
Task: models.Task{
Title: "Task 6",
DueDate: testtimeTime,
},
},
{
Task: models.Task{
Title: "Task 7",
DueDate: testtimeTime,
RepeatAfter: 60 * 60 * 24 * 7, // The amount of seconds in a week
},
},
},
},
{
Task: models.Task{
Title: "Task 6",
DueDate: testtimeTime,
Project: models.Project{
Title: "Project 2",
},
},
{
Task: models.Task{
Title: "Task 7",
DueDate: testtimeTime,
RepeatAfter: 60 * 60 * 24 * 7, // The amount of seconds in a week
},
},
},
},
{
Project: models.Project{
Title: "Project 2",
ID: 3,
ParentProjectID: 1,
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task 1",
},
},
{
Task: models.Task{
Title: "Task 2",
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task 1",
},
},
{
Task: models.Task{
Title: "Task 2",
},
},
},
},
},

View File

@ -28,8 +28,7 @@ type Status struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
UserID int64 `xorm:"bigint not null" json:"-"`
MigratorName string `xorm:"varchar(255)" json:"migrator_name"`
StartedAt time.Time `xorm:"not null" json:"started_at"`
FinishedAt time.Time `xorm:"null" json:"finished_at"`
Created time.Time `xorm:"created not null 'created'" json:"time"`
}
// TableName holds the table name for the migration status table
@ -37,31 +36,19 @@ func (s *Status) TableName() string {
return "migration_status"
}
// StartMigration sets the migration status for a user
func StartMigration(m MigratorName, u *user.User) (status *Status, err error) {
// SetMigrationStatus sets the migration status for a user
func SetMigrationStatus(m MigratorName, u *user.User) (err error) {
s := db.NewSession()
defer s.Close()
status = &Status{
status := &Status{
UserID: u.ID,
MigratorName: m.Name(),
StartedAt: time.Now(),
}
_, err = s.Insert(status)
return
}
// FinishMigration sets the finished at time and calls it a day
func FinishMigration(status *Status) (err error) {
s := db.NewSession()
defer s.Close()
status.FinishedAt = time.Now()
_, err = s.Where("id = ?", status.ID).Update(status)
return
}
// GetMigrationStatus returns the migration status for a migration and a user
func GetMigrationStatus(m MigratorName, u *user.User) (status *Status, err error) {
s := db.NewSession()

View File

@ -75,25 +75,20 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
}
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.ProjectWithTasksAndBuckets) {
var pseudoParentID int64 = 1
result = []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
ID: pseudoParentID,
Title: "Migrated from TickTick",
},
parent := &models.ProjectWithTasksAndBuckets{
Project: models.Project{
Title: "Migrated from TickTick",
},
ChildProjects: []*models.ProjectWithTasksAndBuckets{},
}
projects := make(map[string]*models.ProjectWithTasksAndBuckets)
for index, t := range tasks {
for _, t := range tasks {
_, has := projects[t.ProjectName]
if !has {
projects[t.ProjectName] = &models.ProjectWithTasksAndBuckets{
Project: models.Project{
ID: int64(index+1) + pseudoParentID,
ParentProjectID: pseudoParentID,
Title: t.ProjectName,
Title: t.ProjectName,
},
}
}
@ -139,14 +134,14 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.ProjectWi
}
for _, l := range projects {
result = append(result, l)
parent.ChildProjects = append(parent.ChildProjects, l)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Title < result[j].Title
sort.Slice(parent.ChildProjects, func(i, j int) bool {
return parent.ChildProjects[i].Title < parent.ChildProjects[j].Title
})
return
return []*models.ProjectWithTasksAndBuckets{parent}
}
// Name is used to get the name of the ticktick migration - we're using the docs here to annotate the status route.

View File

@ -86,33 +86,31 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
vikunjaTasks := convertTickTickToVikunja(tickTickTasks)
assert.Len(t, vikunjaTasks, 3)
assert.Len(t, vikunjaTasks, 1)
assert.Len(t, vikunjaTasks[0].ChildProjects, 2)
assert.Equal(t, vikunjaTasks[1].ParentProjectID, vikunjaTasks[0].ID)
assert.Equal(t, vikunjaTasks[2].ParentProjectID, vikunjaTasks[0].ID)
assert.Len(t, vikunjaTasks[0].ChildProjects[0].Tasks, 3)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Title, tickTickTasks[0].ProjectName)
assert.Len(t, vikunjaTasks[1].Tasks, 3)
assert.Equal(t, vikunjaTasks[1].Title, tickTickTasks[0].ProjectName)
assert.Equal(t, vikunjaTasks[1].Tasks[0].Title, tickTickTasks[0].Title)
assert.Equal(t, vikunjaTasks[1].Tasks[0].Description, tickTickTasks[0].Content)
assert.Equal(t, vikunjaTasks[1].Tasks[0].StartDate, tickTickTasks[0].StartDate.Time)
assert.Equal(t, vikunjaTasks[1].Tasks[0].EndDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[1].Tasks[0].DueDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[1].Tasks[0].Labels, []*models.Label{
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Title, tickTickTasks[0].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Description, tickTickTasks[0].Content)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].StartDate, tickTickTasks[0].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].EndDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].DueDate, tickTickTasks[0].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Labels, []*models.Label{
{Title: "label1"},
{Title: "label2"},
})
assert.Equal(t, vikunjaTasks[1].Tasks[0].Reminders[0].RelativeTo, models.ReminderRelation("due_date"))
assert.Equal(t, vikunjaTasks[1].Tasks[0].Reminders[0].RelativePeriod, int64(-24*3600))
assert.Equal(t, vikunjaTasks[1].Tasks[0].Position, tickTickTasks[0].Order)
assert.Equal(t, vikunjaTasks[1].Tasks[0].Done, false)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Reminders[0].RelativeTo, models.ReminderRelation("due_date"))
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Reminders[0].RelativePeriod, int64(-24*3600))
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Position, tickTickTasks[0].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[0].Done, false)
assert.Equal(t, vikunjaTasks[1].Tasks[1].Title, tickTickTasks[1].Title)
assert.Equal(t, vikunjaTasks[1].Tasks[1].Position, tickTickTasks[1].Order)
assert.Equal(t, vikunjaTasks[1].Tasks[1].Done, true)
assert.Equal(t, vikunjaTasks[1].Tasks[1].DoneAt, tickTickTasks[1].CompletedTime.Time)
assert.Equal(t, vikunjaTasks[1].Tasks[1].RelatedTasks, models.RelatedTaskMap{
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].Title, tickTickTasks[1].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].Position, tickTickTasks[1].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].Done, true)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].DoneAt, tickTickTasks[1].CompletedTime.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[1].RelatedTasks, models.RelatedTaskMap{
models.RelationKindParenttask: []*models.Task{
{
ID: tickTickTasks[1].ParentID,
@ -120,24 +118,24 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
},
})
assert.Equal(t, vikunjaTasks[1].Tasks[2].Title, tickTickTasks[2].Title)
assert.Equal(t, vikunjaTasks[1].Tasks[2].Description, tickTickTasks[2].Content)
assert.Equal(t, vikunjaTasks[1].Tasks[2].StartDate, tickTickTasks[2].StartDate.Time)
assert.Equal(t, vikunjaTasks[1].Tasks[2].EndDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[1].Tasks[2].DueDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[1].Tasks[2].Labels, []*models.Label{
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Title, tickTickTasks[2].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Description, tickTickTasks[2].Content)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].StartDate, tickTickTasks[2].StartDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].EndDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].DueDate, tickTickTasks[2].DueDate.Time)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Labels, []*models.Label{
{Title: "label1"},
{Title: "label2"},
{Title: "other label"},
})
assert.Equal(t, vikunjaTasks[1].Tasks[2].Reminders[0].RelativeTo, models.ReminderRelation("due_date"))
assert.Equal(t, vikunjaTasks[1].Tasks[2].Reminders[0].RelativePeriod, int64(-24*3600))
assert.Equal(t, vikunjaTasks[1].Tasks[2].Position, tickTickTasks[2].Order)
assert.Equal(t, vikunjaTasks[1].Tasks[2].Done, false)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Reminders[0].RelativeTo, models.ReminderRelation("due_date"))
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Reminders[0].RelativePeriod, int64(-24*3600))
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Position, tickTickTasks[2].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[0].Tasks[2].Done, false)
assert.Len(t, vikunjaTasks[2].Tasks, 1)
assert.Equal(t, vikunjaTasks[2].Title, tickTickTasks[3].ProjectName)
assert.Len(t, vikunjaTasks[0].ChildProjects[1].Tasks, 1)
assert.Equal(t, vikunjaTasks[0].ChildProjects[1].Title, tickTickTasks[3].ProjectName)
assert.Equal(t, vikunjaTasks[2].Tasks[0].Title, tickTickTasks[3].Title)
assert.Equal(t, vikunjaTasks[2].Tasks[0].Position, tickTickTasks[3].Order)
assert.Equal(t, vikunjaTasks[0].ChildProjects[1].Tasks[0].Title, tickTickTasks[3].Title)
assert.Equal(t, vikunjaTasks[0].ChildProjects[1].Tasks[0].Position, tickTickTasks[3].Order)
}

View File

@ -247,15 +247,11 @@ func parseDate(dateString string) (date time.Time, err error) {
func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
var pseudoParentID int64 = 1
parent := &models.ProjectWithTasksAndBuckets{
Project: models.Project{
ID: pseudoParentID,
Title: "Migrated from todoist",
},
}
fullVikunjaHierachie = append(fullVikunjaHierachie, parent)
// A map for all vikunja lists with the project id they're coming from as key
lists := make(map[string]*models.ProjectWithTasksAndBuckets, len(sync.Projects))
@ -268,20 +264,18 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi
sections := make(map[string]int64)
for index, p := range sync.Projects {
for _, p := range sync.Projects {
project := &models.ProjectWithTasksAndBuckets{
Project: models.Project{
ID: int64(index+1) + pseudoParentID,
ParentProjectID: pseudoParentID,
Title: p.Name,
HexColor: todoistColors[p.Color],
IsArchived: p.IsArchived,
Title: p.Name,
HexColor: todoistColors[p.Color],
IsArchived: p.IsArchived,
},
}
lists[p.ID] = project
fullVikunjaHierachie = append(fullVikunjaHierachie, project)
parent.ChildProjects = append(parent.ChildProjects, project)
}
sort.Slice(sync.Sections, func(i, j int) bool {
@ -477,7 +471,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi
)
}
return
return []*models.ProjectWithTasksAndBuckets{parent}, err
}
func getAccessTokenFromAuthToken(authToken string) (accessToken string, err error) {

View File

@ -254,31 +254,31 @@ func TestConvertTodoistToVikunja(t *testing.T) {
},
ProjectNotes: []*projectNote{
{
ID: "102000",
ID: 102000,
Content: "Lorem Ipsum dolor sit amet",
ProjectID: "396936926",
Posted: time3,
},
{
ID: "102001",
ID: 102001,
Content: "Lorem Ipsum dolor sit amet 2",
ProjectID: "396936926",
Posted: time3,
},
{
ID: "102002",
ID: 102002,
Content: "Lorem Ipsum dolor sit amet 3",
ProjectID: "396936926",
Posted: time3,
},
{
ID: "102003",
ID: 102003,
Content: "Lorem Ipsum dolor sit amet 4",
ProjectID: "396936927",
Posted: time3,
},
{
ID: "102004",
ID: 102004,
Content: "Lorem Ipsum dolor sit amet 5",
ProjectID: "396936927",
Posted: time3,
@ -366,261 +366,256 @@ func TestConvertTodoistToVikunja(t *testing.T) {
expectedHierachie := []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
ID: 1,
Title: "Migrated from todoist",
},
},
{
Project: models.Project{
ID: 2,
ParentProjectID: 1,
Title: "Project1",
Description: "Lorem Ipsum dolor sit amet\nLorem Ipsum dolor sit amet 2\nLorem Ipsum dolor sit amet 3",
HexColor: todoistColors["berry_red"],
},
Buckets: []*models.Bucket{
ChildProjects: []*models.ProjectWithTasksAndBuckets{
{
ID: 1,
Title: "Some Bucket",
},
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task400000000",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
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())},
Project: models.Project{
Title: "Project1",
Description: "Lorem Ipsum dolor sit amet\nLorem Ipsum dolor sit amet 2\nLorem Ipsum dolor sit amet 3",
HexColor: todoistColors["berry_red"],
},
Buckets: []*models.Bucket{
{
ID: 1,
Title: "Some Bucket",
},
},
},
{
Task: models.Task{
Title: "Task400000001",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000002",
Done: false,
Created: time1,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000003",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000004",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000005",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000006",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "Task with parent",
Done: false,
Priority: 2,
Created: time1,
DoneAt: nilTime,
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task400000000",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
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())},
},
},
},
{
Task: models.Task{
Title: "Task400000001",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000002",
Done: false,
Created: time1,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000003",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000004",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000005",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000006",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "Task with parent",
Done: false,
Priority: 2,
Created: time1,
DoneAt: nilTime,
},
},
},
},
},
{
Task: models.Task{
Title: "Task400000106",
Done: true,
DueDate: dueTimeWithTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000107",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000108",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000109",
Done: true,
Created: time1,
DoneAt: time3,
BucketID: 1,
},
},
},
},
{
Task: models.Task{
Title: "Task400000106",
Done: true,
DueDate: dueTimeWithTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
Project: models.Project{
Title: "Project2",
Description: "Lorem Ipsum dolor sit amet 4\nLorem Ipsum dolor sit amet 5",
HexColor: todoistColors["mint_green"],
},
},
{
Task: models.Task{
Title: "Task400000107",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000108",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000109",
Done: true,
Created: time1,
DoneAt: time3,
BucketID: 1,
},
},
},
},
{
Project: models.Project{
ID: 3,
ParentProjectID: 1,
Title: "Project2",
Description: "Lorem Ipsum dolor sit amet 4\nLorem Ipsum dolor sit amet 5",
HexColor: todoistColors["mint_green"],
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task400000007",
Done: false,
DueDate: dueTime,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000008",
Done: false,
DueDate: dueTime,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000009",
Done: false,
Created: time1,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000010",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000101",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file.md",
Mime: "text/plain",
Size: 12345,
Created: time1,
FileContent: exampleFile,
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task400000007",
Done: false,
DueDate: dueTime,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000008",
Done: false,
DueDate: dueTime,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000009",
Done: false,
Created: time1,
Reminders: []*models.TaskReminder{
{Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())},
},
},
},
{
Task: models.Task{
Title: "Task400000010",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000101",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file.md",
Mime: "text/plain",
Size: 12345,
Created: time1,
FileContent: exampleFile,
},
Created: time1,
},
},
},
},
{
Task: models.Task{
Title: "Task400000102",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000103",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000104",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000105",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
},
},
},
},
{
Task: models.Task{
Title: "Task400000102",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
Project: models.Project{
Title: "Project3 - Archived",
HexColor: todoistColors["mint_green"],
IsArchived: true,
},
},
{
Task: models.Task{
Title: "Task400000103",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000104",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000105",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
},
},
},
},
{
Project: models.Project{
ID: 4,
ParentProjectID: 1,
Title: "Project3 - Archived",
HexColor: todoistColors["mint_green"],
IsArchived: true,
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task400000111",
Done: true,
Created: time1,
DoneAt: time3,
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task400000111",
Done: true,
Created: time1,
DoneAt: time3,
},
},
},
},
},

View File

@ -166,13 +166,12 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
log.Debugf("[Trello Migration] ")
var pseudoParentID int64 = 1
fullVikunjaHierachie = []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
ID: pseudoParentID,
Title: "Imported from Trello",
},
ChildProjects: []*models.ProjectWithTasksAndBuckets{},
},
}
@ -180,14 +179,12 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
log.Debugf("[Trello Migration] Converting %d boards to vikunja projects", len(trelloData))
for index, board := range trelloData {
for _, board := range trelloData {
project := &models.ProjectWithTasksAndBuckets{
Project: models.Project{
ID: int64(index+1) + pseudoParentID,
ParentProjectID: pseudoParentID,
Title: board.Name,
Description: board.Desc,
IsArchived: board.Closed,
Title: board.Name,
Description: board.Desc,
IsArchived: board.Closed,
},
}
@ -303,7 +300,7 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board, token string) (fullV
log.Debugf("[Trello Migration] Converted all cards to tasks for board %s", board.ID)
fullVikunjaHierachie = append(fullVikunjaHierachie, project)
fullVikunjaHierachie[0].ChildProjects = append(fullVikunjaHierachie[0].ChildProjects, project)
}
return

View File

@ -190,62 +190,59 @@ func TestConvertTrelloToVikunja(t *testing.T) {
expectedHierachie := []*models.ProjectWithTasksAndBuckets{
{
Project: models.Project{
ID: 1,
Title: "Imported from Trello",
},
},
{
Project: models.Project{
ID: 2,
ParentProjectID: 1,
Title: "TestBoard",
Description: "This is a description",
BackgroundInformation: bytes.NewBuffer(exampleFile),
},
Buckets: []*models.Bucket{
ChildProjects: []*models.ProjectWithTasksAndBuckets{
{
ID: 1,
Title: "Test Project 1",
},
{
ID: 2,
Title: "Test Project 2",
},
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 1",
Description: "Card Description",
BucketID: 1,
KanbanPosition: 123,
DueDate: time1,
Labels: []*models.Label{
{
Title: "Label 1",
HexColor: trelloColorMap["green"],
},
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
Project: models.Project{
Title: "TestBoard",
Description: "This is a description",
BackgroundInformation: bytes.NewBuffer(exampleFile),
},
Buckets: []*models.Bucket{
{
ID: 1,
Title: "Test Project 1",
},
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "Testimage.jpg",
Mime: "image/jpg",
Size: uint64(len(exampleFile)),
FileContent: exampleFile,
{
ID: 2,
Title: "Test Project 2",
},
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 1",
Description: "Card Description",
BucketID: 1,
KanbanPosition: 123,
DueDate: time1,
Labels: []*models.Label{
{
Title: "Label 1",
HexColor: trelloColorMap["green"],
},
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
},
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "Testimage.jpg",
Mime: "image/jpg",
Size: uint64(len(exampleFile)),
FileContent: exampleFile,
},
},
},
},
},
},
},
{
Task: models.Task{
Title: "Test Card 2",
Description: `
{
Task: models.Task{
Title: "Test Card 2",
Description: `
## Checkproject 1
@ -256,108 +253,106 @@ func TestConvertTrelloToVikunja(t *testing.T) {
* [ ] Pending Task
* [ ] Another Pending Task`,
BucketID: 1,
KanbanPosition: 124,
},
},
{
Task: models.Task{
Title: "Test Card 3",
BucketID: 1,
KanbanPosition: 126,
},
},
{
Task: models.Task{
Title: "Test Card 4",
BucketID: 1,
KanbanPosition: 127,
Labels: []*models.Label{
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
BucketID: 1,
KanbanPosition: 124,
},
},
{
Task: models.Task{
Title: "Test Card 3",
BucketID: 1,
KanbanPosition: 126,
},
},
{
Task: models.Task{
Title: "Test Card 4",
BucketID: 1,
KanbanPosition: 127,
Labels: []*models.Label{
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
},
},
},
{
Task: models.Task{
Title: "Test Card 5",
BucketID: 2,
KanbanPosition: 111,
Labels: []*models.Label{
{
Title: "Label 3",
HexColor: trelloColorMap["blue"],
},
},
},
},
{
Task: models.Task{
Title: "Test Card 6",
BucketID: 2,
KanbanPosition: 222,
DueDate: time1,
},
},
{
Task: models.Task{
Title: "Test Card 7",
BucketID: 2,
KanbanPosition: 333,
},
},
{
Task: models.Task{
Title: "Test Card 8",
BucketID: 2,
KanbanPosition: 444,
},
},
},
},
{
Task: models.Task{
Title: "Test Card 5",
BucketID: 2,
KanbanPosition: 111,
Labels: []*models.Label{
{
Title: "Label 3",
HexColor: trelloColorMap["blue"],
Project: models.Project{
Title: "TestBoard 2",
},
Buckets: []*models.Bucket{
{
ID: 3,
Title: "Test Project 4",
},
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 634",
BucketID: 3,
KanbanPosition: 123,
},
},
},
},
{
Task: models.Task{
Title: "Test Card 6",
BucketID: 2,
KanbanPosition: 222,
DueDate: time1,
Project: models.Project{
Title: "TestBoard Archived",
IsArchived: true,
},
},
{
Task: models.Task{
Title: "Test Card 7",
BucketID: 2,
KanbanPosition: 333,
Buckets: []*models.Bucket{
{
ID: 4,
Title: "Test Project 5",
},
},
},
{
Task: models.Task{
Title: "Test Card 8",
BucketID: 2,
KanbanPosition: 444,
},
},
},
},
{
Project: models.Project{
ID: 3,
ParentProjectID: 1,
Title: "TestBoard 2",
},
Buckets: []*models.Bucket{
{
ID: 3,
Title: "Test Project 4",
},
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 634",
BucketID: 3,
KanbanPosition: 123,
},
},
},
},
{
Project: models.Project{
ID: 4,
ParentProjectID: 1,
Title: "TestBoard Archived",
IsArchived: true,
},
Buckets: []*models.Bucket{
{
ID: 4,
Title: "Test Project 5",
},
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 63423",
BucketID: 4,
KanbanPosition: 123,
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Test Card 63423",
BucketID: 4,
KanbanPosition: 123,
},
},
},
},
},

View File

@ -17,7 +17,6 @@
package caldav
import (
"slices"
"strconv"
"strings"
"time"
@ -293,13 +292,6 @@ func (vcls *VikunjaCaldavProjectStorage) CreateResource(rpath, content string) (
return nil, err
}
vcls.task.ProjectID = vcls.project.ID
err = persistRelations(s, vcls.user, vcls.task, vTask.RelatedTasks)
if err != nil {
_ = s.Rollback()
return nil, err
}
if err := s.Commit(); err != nil {
return nil, err
}
@ -324,10 +316,6 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
// At this point, we already have the right task in vcls.task, so we can use that ID directly
vTask.ID = vcls.task.ID
// Explicitly set the ProjectID in case the task now belongs to a different project:
vTask.ProjectID = vcls.project.ID
vcls.task.ProjectID = vcls.project.ID
s := db.NewSession()
defer s.Close()
@ -355,12 +343,6 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
return nil, err
}
err = persistRelations(s, vcls.user, vcls.task, vTask.RelatedTasks)
if err != nil {
_ = s.Rollback()
return nil, err
}
if err := s.Commit(); err != nil {
return nil, err
}
@ -448,91 +430,6 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
return task.UpdateTaskLabels(s, a, labels)
}
func removeStaleRelations(s *xorm.Session, a web.Auth, task *models.Task, newRelations map[models.RelationKind][]*models.Task) (err error) {
// Get the existing task with details:
existingTask := &models.Task{ID: task.ID}
// FIXME: Optimize to get only required attributes (ie. RelatedTasks).
err = existingTask.ReadOne(s, a)
if err != nil {
return
}
for relationKind, relatedTasks := range existingTask.RelatedTasks {
for _, relatedTask := range relatedTasks {
relationInNewList := slices.ContainsFunc(newRelations[relationKind], func(newRelation *models.Task) bool { return newRelation.UID == relatedTask.UID })
if !relationInNewList {
rel := models.TaskRelation{
TaskID: task.ID,
OtherTaskID: relatedTask.ID,
RelationKind: relationKind,
}
err = rel.Delete(s, a)
if err != nil {
return
}
}
}
}
return
}
// Persist new relations provided by the VTODO entry:
func persistRelations(s *xorm.Session, a web.Auth, task *models.Task, newRelations map[models.RelationKind][]*models.Task) (err error) {
err = removeStaleRelations(s, a, task, newRelations)
if err != nil {
return err
}
// Ensure the current relations exist:
for relationType, relatedTasksInVTODO := range newRelations {
// Persist each relation independently:
for _, relatedTaskInVTODO := range relatedTasksInVTODO {
var relatedTask *models.Task
createDummy := false
// Get the task from the DB:
relatedTaskInDB, err := models.GetTaskSimpleByUUID(s, relatedTaskInVTODO.UID)
if err != nil {
relatedTask = relatedTaskInVTODO
createDummy = true
} else {
relatedTask = relatedTaskInDB
}
// If the related task doesn't exist, create a dummy one now in the same list.
// It'll probably be populated right after in a following request.
// In the worst case, this was an error by the client and we are left with
// this dummy task to clean up.
if createDummy {
relatedTask.ProjectID = task.ProjectID
relatedTask.Title = "DUMMY-UID-" + relatedTask.UID
err = relatedTask.Create(s, a)
if err != nil {
return err
}
}
// Create the relation:
rel := models.TaskRelation{
TaskID: task.ID,
OtherTaskID: relatedTask.ID,
RelationKind: relationType,
}
err = rel.Create(s, a)
if err != nil && !models.IsErrRelationAlreadyExists(err) {
return err
}
}
}
return err
}
// VikunjaProjectResourceAdapter holds the actual resource
type VikunjaProjectResourceAdapter struct {
project *models.ProjectWithTasksAndBuckets

View File

@ -1,484 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 caldav
// This file tests logic related to handling tasks in CALDAV format
import (
"testing"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"github.com/samedi/caldav-go/data"
"github.com/stretchr/testify/assert"
)
// Check logic related to creating sub-tasks
func TestSubTask_Create(t *testing.T) {
u := &user.User{
ID: 15,
Username: "user15",
Email: "user15@example.com",
}
config.InitDefaultConfig()
files.InitTests()
user.InitTests()
models.SetupTests()
//
// Create a subtask
//
t.Run("create", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
const taskUID = "uid_child1"
const taskContent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child1
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 1
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task
END:VTODO
END:VCALENDAR`
storage := &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: &models.Task{UID: taskUID},
user: u,
}
// Create the subtask:
taskResource, err := storage.CreateResource(taskUID, taskContent)
assert.NoError(t, err)
// Check that the result CALDAV contains the relation:
content, _ := taskResource.GetContentData()
assert.Contains(t, content, "UID:"+taskUID)
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
// Get the task from the DB:
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task := tasks[0]
// Check that the parent-child relationship is present:
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
assert.Equal(t, "uid-caldav-test-parent-task", parentTask.UID)
})
//
// Create a subtask on a subtask, i.e. create a grand-child
//
t.Run("create grandchild on child task", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
const taskUIDChild = "uid_child1"
const taskContentChild = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child1
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 1
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task
END:VTODO
END:VCALENDAR`
storage := &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: &models.Task{UID: taskUIDChild},
user: u,
}
// Create the subtask:
_, err := storage.CreateResource(taskUIDChild, taskContentChild)
assert.NoError(t, err)
const taskUID = "uid_grand_child1"
const taskContent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_grand_child1
DTSTAMP:20230301T073337Z
SUMMARY:Caldav grand child task 1
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid_child1
END:VTODO
END:VCALENDAR`
storage = &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: &models.Task{UID: taskUID},
user: u,
}
// Create the task:
var taskResource *data.Resource
taskResource, err = storage.CreateResource(taskUID, taskContent)
assert.NoError(t, err)
// Check that the result CALDAV contains the relation:
content, _ := taskResource.GetContentData()
assert.Contains(t, content, "UID:"+taskUID)
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid_child1")
// Get the task from the DB:
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task := tasks[0]
// Check that the parent-child relationship of the grandchildren is present:
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
assert.Equal(t, "uid_child1", parentTask.UID)
// Get the child task and check that it now has a parent and a child:
tasks, err = models.GetTasksByUIDs(s, []string{"uid_child1"}, u)
assert.NoError(t, err)
task = tasks[0]
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
parentTask = task.RelatedTasks[models.RelationKindParenttask][0]
assert.Equal(t, "uid-caldav-test-parent-task", parentTask.UID)
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 1)
childTask := task.RelatedTasks[models.RelationKindSubtask][0]
assert.Equal(t, taskUID, childTask.UID)
})
//
// Create a subtask on a parent that we don't know anything about (yet)
//
t.Run("create subtask on unknown parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Create a subtask:
const taskUID = "uid_child1"
const taskContent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid_child1
DTSTAMP:20230301T073337Z
SUMMARY:Caldav child task 1
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-doesnt-exist-yet
END:VTODO
END:VCALENDAR`
storage := &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: &models.Task{UID: taskUID},
user: u,
}
// Create the task:
taskResource, err := storage.CreateResource(taskUID, taskContent)
assert.NoError(t, err)
// Check that the result CALDAV contains the relation:
content, _ := taskResource.GetContentData()
assert.Contains(t, content, "UID:"+taskUID)
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-doesnt-exist-yet")
// Get the task from the DB:
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task := tasks[0]
// Check that the parent-child relationship is present:
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
assert.Equal(t, "uid-caldav-test-parent-doesnt-exist-yet", parentTask.UID)
// Check that the non-existent parent task was created in the process:
tasks, err = models.GetTasksByUIDs(s, []string{"uid-caldav-test-parent-doesnt-exist-yet"}, u)
assert.NoError(t, err)
task = tasks[0]
assert.Equal(t, "uid-caldav-test-parent-doesnt-exist-yet", task.UID)
})
}
// Logic related to editing tasks and subtasks
func TestSubTask_Update(t *testing.T) {
u := &user.User{
ID: 15,
Username: "user15",
Email: "user15@example.com",
}
//
// Edit a subtask and check that the relations are not gone
//
t.Run("edit subtask", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Edit the subtask:
const taskUID = "uid-caldav-test-child-task"
const taskContent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid-caldav-test-child-task
DTSTAMP:20230301T073337Z
SUMMARY:Child task for Caldav Test (edited)
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task
END:VTODO
END:VCALENDAR`
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task := tasks[0]
storage := &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: task,
user: u,
}
// Edit the task:
taskResource, err := storage.UpdateResource(taskUID, taskContent)
assert.NoError(t, err)
// Check that the result CALDAV still contains the relation:
content, _ := taskResource.GetContentData()
assert.Contains(t, content, "UID:"+taskUID)
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
// Get the task from the DB:
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task = tasks[0]
// Check that the parent-child relationship is still present:
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
assert.Equal(t, "uid-caldav-test-parent-task", parentTask.UID)
})
//
// Edit a parent task and check that the subtasks are still linked
//
t.Run("edit parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Edit the parent task:
const taskUID = "uid-caldav-test-parent-task"
const taskContent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid-caldav-test-parent-task
DTSTAMP:20230301T073337Z
SUMMARY:Parent task for Caldav Test (edited)
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task
RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-2
END:VTODO
END:VCALENDAR`
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task := tasks[0]
storage := &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: task,
user: u,
}
// Edit the task:
_, err = storage.UpdateResource(taskUID, taskContent)
assert.NoError(t, err)
// Get the task from the DB:
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task = tasks[0]
// Check that the subtasks are still linked:
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 2)
existingSubTask := task.RelatedTasks[models.RelationKindSubtask][0]
assert.Equal(t, "uid-caldav-test-child-task", existingSubTask.UID)
existingSubTask = task.RelatedTasks[models.RelationKindSubtask][1]
assert.Equal(t, "uid-caldav-test-child-task-2", existingSubTask.UID)
})
//
// Edit a subtask and change its parent
//
t.Run("edit subtask change parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Edit the subtask:
const taskUID = "uid-caldav-test-child-task"
const taskContent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid-caldav-test-child-task
DTSTAMP:20230301T073337Z
SUMMARY:Child task for Caldav Test (edited)
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-2
END:VTODO
END:VCALENDAR`
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task := tasks[0]
storage := &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: task,
user: u,
}
// Edit the task:
taskResource, err := storage.UpdateResource(taskUID, taskContent)
assert.NoError(t, err)
// Check that the result CALDAV contains the new relation:
content, _ := taskResource.GetContentData()
assert.Contains(t, content, "UID:"+taskUID)
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-2")
// Get the task from the DB:
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task = tasks[0]
// Check that the parent-child relationship has changed to the new parent:
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
assert.Equal(t, "uid-caldav-test-parent-task-2", parentTask.UID)
// Get the previous parent from the DB and check that its previous child is gone:
tasks, err = models.GetTasksByUIDs(s, []string{"uid-caldav-test-parent-task"}, u)
assert.NoError(t, err)
task = tasks[0]
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 1)
// We're gone, but our former sibling is still there:
formerSiblingSubTask := task.RelatedTasks[models.RelationKindSubtask][0]
assert.Equal(t, "uid-caldav-test-child-task-2", formerSiblingSubTask.UID)
})
//
// Edit a subtask and remove its parent
//
t.Run("edit subtask remove parent", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Edit the subtask:
const taskUID = "uid-caldav-test-child-task"
const taskContent = `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:Project 36 for Caldav tests
PRODID:-//Vikunja Todo App//EN
BEGIN:VTODO
UID:uid-caldav-test-child-task
DTSTAMP:20230301T073337Z
SUMMARY:Child task for Caldav Test (edited)
CREATED:20230301T073337Z
LAST-MODIFIED:20230301T073337Z
END:VTODO
END:VCALENDAR`
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task := tasks[0]
storage := &VikunjaCaldavProjectStorage{
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
task: task,
user: u,
}
// Edit the task:
taskResource, err := storage.UpdateResource(taskUID, taskContent)
assert.NoError(t, err)
// Check that the result CALDAV contains the new relation:
content, _ := taskResource.GetContentData()
assert.Contains(t, content, "UID:"+taskUID)
assert.NotContains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
// Get the task from the DB:
tasks, err = models.GetTasksByUIDs(s, []string{taskUID}, u)
assert.NoError(t, err)
task = tasks[0]
// Check that the parent-child relationship is gone:
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 0)
// Get the previous parent from the DB and check that its child is gone:
tasks, err = models.GetTasksByUIDs(s, []string{"uid-caldav-test-parent-task"}, u)
assert.NoError(t, err)
task = tasks[0]
// We're gone, but our former sibling is still there:
assert.Len(t, task.RelatedTasks[models.RelationKindSubtask], 1)
formerSiblingSubTask := task.RelatedTasks[models.RelationKindSubtask][0]
assert.Equal(t, "uid-caldav-test-child-task-2", formerSiblingSubTask.UID)
})
}

View File

@ -20,13 +20,11 @@ import (
"crypto/subtle"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/models"
auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/user"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
@ -40,8 +38,8 @@ func setupMetrics(a *echo.Group) {
metrics.InitMetrics()
type countable struct {
Key string
Type interface{}
Rediskey string
Type interface{}
}
for _, c := range []countable{
@ -61,21 +59,13 @@ func setupMetrics(a *echo.Group) {
metrics.TeamCountKey,
models.Team{},
},
{
metrics.FilesCountKey,
files.File{},
},
{
metrics.AttachmentsCountKey,
models.TaskAttachment{},
},
} {
// Set initial totals
total, err := models.GetTotalCount(c.Type)
if err != nil {
log.Fatalf("Could not get initial count for %v, error was %s", c.Type, err)
}
if err := metrics.SetCount(total, c.Key); err != nil {
if err := metrics.SetCount(total, c.Rediskey); err != nil {
log.Fatalf("Could not set initial count for %v, error was %s", c.Type, err)
}
}
@ -120,9 +110,5 @@ func updateActiveUsersFromContext(c echo.Context) (err error) {
return
}
if _, is := auth.(*models.LinkSharing); is {
return metrics.SetLinkShareActive(auth)
}
return metrics.SetUserActive(auth)
}

View File

@ -246,7 +246,7 @@ func registerAPIRoutes(a *echo.Group) {
ur := a.Group("")
rate := limiter.Rate{
Period: 60 * time.Second,
Limit: config.RateLimitNoAuthRoutesLimit.GetInt64(),
Limit: 10,
}
rateLimiter := createRateLimiter(rate)
ur.Use(RateLimit(rateLimiter, "ip"))
@ -599,7 +599,7 @@ func registerMigrations(m *echo.Group) {
return &todoist.Migration{}
},
}
todoistMigrationHandler.RegisterMigrator(m)
todoistMigrationHandler.RegisterRoutes(m)
}
// Trello
@ -609,7 +609,7 @@ func registerMigrations(m *echo.Group) {
return &trello.Migration{}
},
}
trelloMigrationHandler.RegisterMigrator(m)
trelloMigrationHandler.RegisterRoutes(m)
}
// Microsoft Todo
@ -619,7 +619,7 @@ func registerMigrations(m *echo.Group) {
return &microsofttodo.Migration{}
},
}
microsoftTodoMigrationHandler.RegisterMigrator(m)
microsoftTodoMigrationHandler.RegisterRoutes(m)
}
// Vikunja File Migrator

View File

@ -2112,110 +2112,6 @@ const docTemplate = `{
}
},
"/projects/{id}/tasks": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns all tasks for the current project.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get tasks in a project",
"parameters": [
{
"type": "integer",
"description": "The project ID.",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search tasks by task text.",
"name": "s",
"in": "query"
},
{
"type": "string",
"description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with ` + "`" + `order_by` + "`" + `. Possible values to sort by are ` + "`" + `id` + "`" + `, ` + "`" + `title` + "`" + `, ` + "`" + `description` + "`" + `, ` + "`" + `done` + "`" + `, ` + "`" + `done_at` + "`" + `, ` + "`" + `due_date` + "`" + `, ` + "`" + `created_by_id` + "`" + `, ` + "`" + `project_id` + "`" + `, ` + "`" + `repeat_after` + "`" + `, ` + "`" + `priority` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, ` + "`" + `hex_color` + "`" + `, ` + "`" + `percent_done` + "`" + `, ` + "`" + `uid` + "`" + `, ` + "`" + `created` + "`" + `, ` + "`" + `updated` + "`" + `. Default is ` + "`" + `id` + "`" + `.",
"name": "sort_by",
"in": "query"
},
{
"type": "string",
"description": "The ordering parameter. Possible values to order by are ` + "`" + `asc` + "`" + ` or ` + "`" + `desc` + "`" + `. Default is ` + "`" + `asc` + "`" + `.",
"name": "order_by",
"in": "query"
},
{
"type": "string",
"description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.",
"name": "filter_by",
"in": "query"
},
{
"type": "string",
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like ` + "`" + `due_date` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, etc.",
"name": "filter_value",
"in": "query"
},
{
"type": "string",
"description": "The comparator to use for a filter. Available values are ` + "`" + `equals` + "`" + `, ` + "`" + `greater` + "`" + `, ` + "`" + `greater_equals` + "`" + `, ` + "`" + `less` + "`" + `, ` + "`" + `less_equals` + "`" + `, ` + "`" + `like` + "`" + ` and ` + "`" + `in` + "`" + `. ` + "`" + `in` + "`" + ` expects comma-separated values in ` + "`" + `filter_value` + "`" + `. Defaults to ` + "`" + `equals` + "`" + `",
"name": "filter_comparator",
"in": "query"
},
{
"type": "string",
"description": "The concatinator to use for filters. Available values are ` + "`" + `and` + "`" + ` or ` + "`" + `or` + "`" + `. Defaults to ` + "`" + `or` + "`" + `.",
"name": "filter_concat",
"in": "query"
},
{
"type": "string",
"description": "If set to true the result will include filtered fields whose value is set to ` + "`" + `null` + "`" + `. Available values are ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `. Defaults to ` + "`" + `false` + "`" + `.",
"name": "filter_include_nulls",
"in": "query"
}
],
"responses": {
"200": {
"description": "The tasks",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Task"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": {
"security": [
{
@ -2968,6 +2864,112 @@ const docTemplate = `{
}
}
},
"/projects/{projectID}/tasks": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns all tasks for the current project.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get tasks in a project",
"parameters": [
{
"type": "integer",
"description": "The project ID.",
"name": "projectID",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search tasks by task text.",
"name": "s",
"in": "query"
},
{
"type": "string",
"description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with ` + "`" + `order_by` + "`" + `. Possible values to sort by are ` + "`" + `id` + "`" + `, ` + "`" + `title` + "`" + `, ` + "`" + `description` + "`" + `, ` + "`" + `done` + "`" + `, ` + "`" + `done_at` + "`" + `, ` + "`" + `due_date` + "`" + `, ` + "`" + `created_by_id` + "`" + `, ` + "`" + `project_id` + "`" + `, ` + "`" + `repeat_after` + "`" + `, ` + "`" + `priority` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, ` + "`" + `hex_color` + "`" + `, ` + "`" + `percent_done` + "`" + `, ` + "`" + `uid` + "`" + `, ` + "`" + `created` + "`" + `, ` + "`" + `updated` + "`" + `. Default is ` + "`" + `id` + "`" + `.",
"name": "sort_by",
"in": "query"
},
{
"type": "string",
"description": "The ordering parameter. Possible values to order by are ` + "`" + `asc` + "`" + ` or ` + "`" + `desc` + "`" + `. Default is ` + "`" + `asc` + "`" + `.",
"name": "order_by",
"in": "query"
},
{
"type": "string",
"description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.",
"name": "filter_by",
"in": "query"
},
{
"type": "string",
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like ` + "`" + `due_date` + "`" + `, ` + "`" + `start_date` + "`" + `, ` + "`" + `end_date` + "`" + `, etc.",
"name": "filter_value",
"in": "query"
},
{
"type": "string",
"description": "The comparator to use for a filter. Available values are ` + "`" + `equals` + "`" + `, ` + "`" + `greater` + "`" + `, ` + "`" + `greater_equals` + "`" + `, ` + "`" + `less` + "`" + `, ` + "`" + `less_equals` + "`" + `, ` + "`" + `like` + "`" + ` and ` + "`" + `in` + "`" + `. ` + "`" + `in` + "`" + ` expects comma-separated values in ` + "`" + `filter_value` + "`" + `. Defaults to ` + "`" + `equals` + "`" + `",
"name": "filter_comparator",
"in": "query"
},
{
"type": "string",
"description": "The concatinator to use for filters. Available values are ` + "`" + `and` + "`" + ` or ` + "`" + `or` + "`" + `. Defaults to ` + "`" + `or` + "`" + `.",
"name": "filter_concat",
"in": "query"
},
{
"type": "string",
"description": "If set to true the result will include filtered fields whose value is set to ` + "`" + `null` + "`" + `. Available values are ` + "`" + `true` + "`" + ` or ` + "`" + `false` + "`" + `. Defaults to ` + "`" + `false` + "`" + `.",
"name": "filter_include_nulls",
"in": "query"
}
],
"responses": {
"200": {
"description": "The tasks",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Task"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/projects/{projectID}/teams/{teamID}": {
"post": {
"security": [
@ -3883,54 +3885,7 @@ const docTemplate = `{
}
}
},
"/tasks/{id}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns one task by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get one task",
"parameters": [
{
"type": "integer",
"description": "The task ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The task",
"schema": {
"$ref": "#/definitions/models.Task"
}
},
"404": {
"description": "Task not found",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
},
"/tasks/{ID}": {
"post": {
"security": [
{
@ -3952,7 +3907,7 @@ const docTemplate = `{
{
"type": "integer",
"description": "The Task ID",
"name": "id",
"name": "ID",
"in": "path",
"required": true
},
@ -4011,7 +3966,7 @@ const docTemplate = `{
{
"type": "integer",
"description": "Task ID",
"name": "id",
"name": "ID",
"in": "path",
"required": true
}
@ -4044,6 +3999,55 @@ const docTemplate = `{
}
}
},
"/tasks/{id}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns one task by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get one task",
"parameters": [
{
"type": "integer",
"description": "The task ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The task",
"schema": {
"$ref": "#/definitions/models.Task"
}
},
"404": {
"description": "Task not found",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/tasks/{id}/attachments": {
"get": {
"security": [
@ -7183,16 +7187,13 @@ const docTemplate = `{
"migration.Status": {
"type": "object",
"properties": {
"finished_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"migrator_name": {
"type": "string"
},
"started_at": {
"time": {
"type": "string"
}
}

View File

@ -2104,110 +2104,6 @@
}
},
"/projects/{id}/tasks": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns all tasks for the current project.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get tasks in a project",
"parameters": [
{
"type": "integer",
"description": "The project ID.",
"name": "id",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search tasks by task text.",
"name": "s",
"in": "query"
},
{
"type": "string",
"description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`.",
"name": "sort_by",
"in": "query"
},
{
"type": "string",
"description": "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`.",
"name": "order_by",
"in": "query"
},
{
"type": "string",
"description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.",
"name": "filter_by",
"in": "query"
},
{
"type": "string",
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc.",
"name": "filter_value",
"in": "query"
},
{
"type": "string",
"description": "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`",
"name": "filter_comparator",
"in": "query"
},
{
"type": "string",
"description": "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`.",
"name": "filter_concat",
"in": "query"
},
{
"type": "string",
"description": "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`.",
"name": "filter_include_nulls",
"in": "query"
}
],
"responses": {
"200": {
"description": "The tasks",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Task"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": {
"security": [
{
@ -2960,6 +2856,112 @@
}
}
},
"/projects/{projectID}/tasks": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns all tasks for the current project.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get tasks in a project",
"parameters": [
{
"type": "integer",
"description": "The project ID.",
"name": "projectID",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page.",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search tasks by task text.",
"name": "s",
"in": "query"
},
{
"type": "string",
"description": "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`.",
"name": "sort_by",
"in": "query"
},
{
"type": "string",
"description": "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`.",
"name": "order_by",
"in": "query"
},
{
"type": "string",
"description": "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match.",
"name": "filter_by",
"in": "query"
},
{
"type": "string",
"description": "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc.",
"name": "filter_value",
"in": "query"
},
{
"type": "string",
"description": "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`",
"name": "filter_comparator",
"in": "query"
},
{
"type": "string",
"description": "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`.",
"name": "filter_concat",
"in": "query"
},
{
"type": "string",
"description": "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`.",
"name": "filter_include_nulls",
"in": "query"
}
],
"responses": {
"200": {
"description": "The tasks",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.Task"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/projects/{projectID}/teams/{teamID}": {
"post": {
"security": [
@ -3875,54 +3877,7 @@
}
}
},
"/tasks/{id}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns one task by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get one task",
"parameters": [
{
"type": "integer",
"description": "The task ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The task",
"schema": {
"$ref": "#/definitions/models.Task"
}
},
"404": {
"description": "Task not found",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
},
"/tasks/{ID}": {
"post": {
"security": [
{
@ -3944,7 +3899,7 @@
{
"type": "integer",
"description": "The Task ID",
"name": "id",
"name": "ID",
"in": "path",
"required": true
},
@ -4003,7 +3958,7 @@
{
"type": "integer",
"description": "Task ID",
"name": "id",
"name": "ID",
"in": "path",
"required": true
}
@ -4036,6 +3991,55 @@
}
}
},
"/tasks/{id}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns one task by its ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"task"
],
"summary": "Get one task",
"parameters": [
{
"type": "integer",
"description": "The task ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The task",
"schema": {
"$ref": "#/definitions/models.Task"
}
},
"404": {
"description": "Task not found",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/tasks/{id}/attachments": {
"get": {
"security": [
@ -7175,16 +7179,13 @@
"migration.Status": {
"type": "object",
"properties": {
"finished_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"migrator_name": {
"type": "string"
},
"started_at": {
"time": {
"type": "string"
}
}

View File

@ -45,13 +45,11 @@ definitions:
type: object
migration.Status:
properties:
finished_at:
type: string
id:
type: integer
migrator_name:
type: string
started_at:
time:
type: string
type: object
models.APIPermissions:
@ -2837,93 +2835,6 @@ paths:
tags:
- project
/projects/{id}/tasks:
get:
consumes:
- application/json
description: Returns all tasks for the current project.
parameters:
- description: The project ID.
in: path
name: id
required: true
type: integer
- description: The page number. Used for pagination. If not provided, the first
page of results is returned.
in: query
name: page
type: integer
- description: The maximum number of items per page. Note this parameter is
limited by the configured maximum of items per page.
in: query
name: per_page
type: integer
- description: Search tasks by task text.
in: query
name: s
type: string
- description: The sorting parameter. You can pass this multiple times to get
the tasks ordered by multiple different parametes, along with `order_by`.
Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`,
`due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`,
`end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default
is `id`.
in: query
name: sort_by
type: string
- description: The ordering parameter. Possible values to order by are `asc`
or `desc`. Default is `asc`.
in: query
name: order_by
type: string
- description: The name of the field to filter by. Allowed values are all task
properties. Task properties which are their own object require passing in
the id of that entity. Accepts an array for multiple filters which will
be chanied together, all supplied filter must match.
in: query
name: filter_by
type: string
- description: The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)-
or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style
relative dates for all date fields like `due_date`, `start_date`, `end_date`,
etc.
in: query
name: filter_value
type: string
- description: The comparator to use for a filter. Available values are `equals`,
`greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in`
expects comma-separated values in `filter_value`. Defaults to `equals`
in: query
name: filter_comparator
type: string
- description: The concatinator to use for filters. Available values are `and`
or `or`. Defaults to `or`.
in: query
name: filter_concat
type: string
- description: If set to true the result will include filtered fields whose
value is set to `null`. Available values are `true` or `false`. Defaults
to `false`.
in: query
name: filter_include_nulls
type: string
produces:
- application/json
responses:
"200":
description: The tasks
schema:
items:
$ref: '#/definitions/models.Task'
type: array
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get tasks in a project
tags:
- task
put:
consumes:
- application/json
@ -3588,6 +3499,94 @@ paths:
summary: Duplicate an existing project
tags:
- project
/projects/{projectID}/tasks:
get:
consumes:
- application/json
description: Returns all tasks for the current project.
parameters:
- description: The project ID.
in: path
name: projectID
required: true
type: integer
- description: The page number. Used for pagination. If not provided, the first
page of results is returned.
in: query
name: page
type: integer
- description: The maximum number of items per page. Note this parameter is
limited by the configured maximum of items per page.
in: query
name: per_page
type: integer
- description: Search tasks by task text.
in: query
name: s
type: string
- description: The sorting parameter. You can pass this multiple times to get
the tasks ordered by multiple different parametes, along with `order_by`.
Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`,
`due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`,
`end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default
is `id`.
in: query
name: sort_by
type: string
- description: The ordering parameter. Possible values to order by are `asc`
or `desc`. Default is `asc`.
in: query
name: order_by
type: string
- description: The name of the field to filter by. Allowed values are all task
properties. Task properties which are their own object require passing in
the id of that entity. Accepts an array for multiple filters which will
be chanied together, all supplied filter must match.
in: query
name: filter_by
type: string
- description: The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)-
or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style
relative dates for all date fields like `due_date`, `start_date`, `end_date`,
etc.
in: query
name: filter_value
type: string
- description: The comparator to use for a filter. Available values are `equals`,
`greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in`
expects comma-separated values in `filter_value`. Defaults to `equals`
in: query
name: filter_comparator
type: string
- description: The concatinator to use for filters. Available values are `and`
or `or`. Defaults to `or`.
in: query
name: filter_concat
type: string
- description: If set to true the result will include filtered fields whose
value is set to `null`. Available values are `true` or `false`. Defaults
to `false`.
in: query
name: filter_include_nulls
type: string
produces:
- application/json
responses:
"200":
description: The tasks
schema:
items:
$ref: '#/definitions/models.Task'
type: array
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get tasks in a project
tags:
- task
/projects/{projectID}/teams/{teamID}:
delete:
description: Delets a team from a project. The team won't have access to the
@ -3923,13 +3922,13 @@ paths:
summary: Subscribes the current user to an entity.
tags:
- subscriptions
/tasks/{id}:
/tasks/{ID}:
delete:
description: Deletes a task from a project. This does not mean "mark it done".
parameters:
- description: Task ID
in: path
name: id
name: ID
required: true
type: integer
produces:
@ -3956,36 +3955,6 @@ paths:
summary: Delete a task
tags:
- task
get:
consumes:
- application/json
description: Returns one task by its ID
parameters:
- description: The task ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The task
schema:
$ref: '#/definitions/models.Task'
"404":
description: Task not found
schema:
$ref: '#/definitions/models.Message'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get one task
tags:
- task
post:
consumes:
- application/json
@ -3995,7 +3964,7 @@ paths:
parameters:
- description: The Task ID
in: path
name: id
name: ID
required: true
type: integer
- description: The task object
@ -4028,6 +3997,37 @@ paths:
summary: Update a task
tags:
- task
/tasks/{id}:
get:
consumes:
- application/json
description: Returns one task by its ID
parameters:
- description: The task ID
in: path
name: id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The task
schema:
$ref: '#/definitions/models.Task'
"404":
description: Task not found
schema:
$ref: '#/definitions/models.Message'
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get one task
tags:
- task
/tasks/{id}/attachments:
get:
consumes:

View File

@ -146,7 +146,7 @@ type ErrNoPasswordResetToken struct {
}
func (err ErrNoPasswordResetToken) Error() string {
return fmt.Sprintf("No token to reset a password [ID: %d]", err.UserID)
return fmt.Sprintf("No token to reset a password [UserID: %d]", err.UserID)
}
// ErrCodeNoPasswordResetToken holds the unique world-error code of this error
@ -237,7 +237,7 @@ type ErrEmailNotConfirmed struct {
}
func (err ErrEmailNotConfirmed) Error() string {
return fmt.Sprintf("Email is not confirmed [ID: %d]", err.UserID)
return fmt.Sprintf("Email is not confirmed [UserID: %d]", err.UserID)
}
// ErrCodeEmailNotConfirmed holds the unique world-error code of this error