forked from vikunja/vikunja
Compare commits
1 Commits
main
...
renovate/g
Author | SHA1 | Date | |
---|---|---|---|
89347ab5fe |
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
18
go.mod
|
@ -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
36
go.sum
|
@ -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=
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 µsofttodo.Migration{}
|
||||
},
|
||||
}
|
||||
microsoftTodoMigrationHandler.RegisterMigrator(m)
|
||||
microsoftTodoMigrationHandler.RegisterRoutes(m)
|
||||
}
|
||||
|
||||
// Vikunja File Migrator
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user