Merge branch 'main' into renovate/github.com-go-redis-redis-v8-8.x
# Conflicts: # go.mod
This commit is contained in:
commit
7c6c767e70
22
.drone1.yml
22
.drone1.yml
|
@ -772,21 +772,23 @@ trigger:
|
|||
- "refs/tags/**"
|
||||
|
||||
depends_on:
|
||||
- testing
|
||||
- release
|
||||
- deploy-docs
|
||||
- docker-arm-release
|
||||
- docker-amd64-release
|
||||
- docker-manifest
|
||||
|
||||
steps:
|
||||
- name: telegram
|
||||
image: appleboy/drone-telegram:1-linux-amd64
|
||||
- name: notify
|
||||
image: plugins/matrix
|
||||
settings:
|
||||
token:
|
||||
from_secret: TELEGRAM_TOKEN
|
||||
to:
|
||||
from_secret: TELEGRAM_TO
|
||||
message: >
|
||||
{{repo.owner}}/{{repo.name}}: \[{{build.status}}] Build {{build.number}}
|
||||
{{commit.author}} pushed to {{commit.branch}} {{commit.sha}}: `{{commit.message}}`
|
||||
Build started at {{datetime build.started "2006-Jan-02T15:04:05Z" "GMT+2"}} finished at {{datetime build.finished "2006-Jan-02T15:04:05Z" "GMT+2"}}.
|
||||
homeserver: https://matrix.org
|
||||
roomid: WqBDCxzghKcNflkErL:matrix.org
|
||||
username:
|
||||
from_secret: matrix_username
|
||||
password:
|
||||
from_secret: matrix_password
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
|
|
|
@ -23,3 +23,4 @@ files/
|
|||
vikunja-dump*
|
||||
vendor/
|
||||
os-packages/
|
||||
mage_output_file.go
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
systemctl enable vikunja.service
|
||||
|
||||
# Fix the config to contain proper values
|
||||
NEW_SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
|
||||
sed -i "s/<jwt-secret>/$NEW_SECRET/g" /etc/vikunja/config.yml
|
||||
|
|
|
@ -131,10 +131,11 @@ This document describes the different errors Vikunja can return.
|
|||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------|
|
||||
| 10001 | 404 | The bucket does not exist. |
|
||||
| 10002 | 400 | The bucket does not belong to that list. |
|
||||
| 10003 | 412 | You cannot remove the last bucket on a list. |
|
||||
| 10004 | 412 | You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold. |
|
||||
| 10001 | 404 | The bucket does not exist. |
|
||||
| 10002 | 400 | The bucket does not belong to that list. |
|
||||
| 10003 | 412 | You cannot remove the last bucket on a list. |
|
||||
| 10004 | 412 | You cannot add the task to this bucket as it already exceeded the limit of tasks it can hold. |
|
||||
| 10005 | 412 | There can be only one done bucket per list. |
|
||||
|
||||
## Saved Filters
|
||||
|
||||
|
|
36
go.mod
36
go.mod
|
@ -21,33 +21,33 @@ require (
|
|||
code.vikunja.io/web v0.0.0-20210131201003-26386be9a9ae
|
||||
gitea.com/xorm/xorm-redis-cache v0.2.0
|
||||
github.com/ThreeDotsLabs/watermill v1.1.1
|
||||
github.com/adlio/trello v1.8.0
|
||||
github.com/adlio/trello v1.9.0
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
||||
github.com/client9/misspell v0.3.4
|
||||
github.com/client9/misspell v0.3.4 // indirect
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/cweill/gotests v1.6.0
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
|
||||
github.com/fzipp/gocyclo v0.3.1
|
||||
github.com/gabriel-vasile/mimetype v1.1.2
|
||||
github.com/fzipp/gocyclo v0.3.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.2.0
|
||||
github.com/getsentry/sentry-go v0.10.0
|
||||
github.com/go-errors/errors v1.1.1 // indirect
|
||||
github.com/go-redis/redis/v8 v8.7.1
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.5.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/golang/snappy v0.0.2 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254 // indirect
|
||||
github.com/iancoleman/strcase v0.1.3
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/jgautheron/goconst v1.4.0
|
||||
github.com/jgautheron/goconst v1.4.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.2.1
|
||||
github.com/labstack/echo/v4 v4.2.2
|
||||
github.com/labstack/gommon v0.3.0
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
|
||||
github.com/lib/pq v1.10.0
|
||||
|
@ -62,12 +62,12 @@ require (
|
|||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
github.com/prometheus/client_golang v1.10.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546
|
||||
github.com/spf13/afero v1.5.1
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 // indirect
|
||||
github.com/spf13/afero v1.6.0
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
|
@ -75,14 +75,12 @@ require (
|
|||
github.com/stretchr/testify v1.7.0
|
||||
github.com/swaggo/swag v1.7.0
|
||||
github.com/ulule/limiter/v3 v3.8.0
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
|
@ -91,8 +89,8 @@ require (
|
|||
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c
|
||||
honnef.co/go/tools v0.0.1-2020.1.5
|
||||
src.techknowlogick.com/xgo v1.3.1-0.20210218015915-6a603afeb960
|
||||
honnef.co/go/tools v0.0.1-2020.1.5 // indirect
|
||||
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864
|
||||
src.techknowlogick.com/xormigrate v1.4.0
|
||||
xorm.io/builder v0.3.8
|
||||
xorm.io/core v0.7.3
|
||||
|
|
39
go.sum
39
go.sum
|
@ -195,6 +195,8 @@ github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc=
|
|||
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
|
||||
github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU=
|
||||
github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
|
||||
github.com/gabriel-vasile/mimetype v1.2.0 h1:A6z5J8OhjiWFV91sQ3dMI8apYu/tvP9keDaMM3Xu6p4=
|
||||
github.com/gabriel-vasile/mimetype v1.2.0/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
|
||||
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
|
||||
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
|
@ -246,6 +248,8 @@ github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZp
|
|||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.5.0 h1:fFJGHhFdcwy48oTLHvr0WRQ09rGiZE+as9ElvbRWS+c=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.5.0/go.mod h1:P4L3WxgOsCLbAeUC50qX5rdj1ULZfUMqgCbqah3OH5U=
|
||||
|
@ -492,6 +496,8 @@ github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz
|
|||
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
|
||||
github.com/labstack/echo/v4 v4.2.1 h1:LF5Iq7t/jrtUuSutNuiEWtB5eiHfZ5gSe2pcu5exjQw=
|
||||
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
|
||||
github.com/labstack/echo/v4 v4.2.2 h1:bq2fdZCionY1jck8rzUpQEu2YSmI8QbX6LHrCa60IVs=
|
||||
github.com/labstack/echo/v4 v4.2.2/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
|
||||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
|
||||
|
@ -665,6 +671,8 @@ github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNja
|
|||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
|
||||
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
|
||||
github.com/prometheus/client_golang v1.10.0 h1:/o0BDeWzLWXNZ+4q5gXltUvaMpJqckTa+jTNoB+z4cg=
|
||||
github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU=
|
||||
|
@ -686,6 +694,8 @@ github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lN
|
|||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
|
||||
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y=
|
||||
github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg=
|
||||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
|
@ -697,6 +707,8 @@ github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFB
|
|||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
|
||||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
|
@ -742,6 +754,8 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
|||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg=
|
||||
github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
|
||||
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
|
@ -882,6 +896,12 @@ golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rB
|
|||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b h1:wSOdpTq0/eI46Ez/LkDwIsAKA71YP2SRKBODiRWM0as=
|
||||
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -972,6 +992,8 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/Lt
|
|||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
|
@ -980,6 +1002,14 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
|
|||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 h1:alLDrZkL34Y2bnGHfvC1CYBRBXCXgx8AC2vY4MRtYX4=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210311163135-5366d9dc1934 h1:Y2nxrNrrWOZn5yjDEEVU3R7V9HGW5SWsw6B6YL/ZRFw=
|
||||
golang.org/x/oauth2 v0.0.0-20210311163135-5366d9dc1934/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84 h1:duBc5zuJsmJXYOVVE/6PxejI+N3AaCqKjtsoLn1Je5Q=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -993,6 +1023,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2By
|
|||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -1060,11 +1091,17 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||
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-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 h1:VqE9gduFZ4dbR7XoL77lHFp0/DyDUBKSXK7CMFkVcV0=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1316,6 +1353,8 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
|||
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
|
||||
src.techknowlogick.com/xgo v1.3.1-0.20210218015915-6a603afeb960 h1:885qVTLUDXe985P1nNf50jb5Up02igmxt7sNuE/4W/E=
|
||||
src.techknowlogick.com/xgo v1.3.1-0.20210218015915-6a603afeb960/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864 h1:wBdOhmwnc6zZZzlGdhZLxBk2yDzKcQoqB5C9fePlORM=
|
||||
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xormigrate v1.4.0 h1:gAfLoDwcVfMiFhSXg5Qwm7LNnG1iUbBVDUNfHagDLQc=
|
||||
src.techknowlogick.com/xormigrate v1.4.0/go.mod h1:xCtbAK00lJ0v4zP2O6VBVMG3RHm7W5Yo1Dz0r9kL/ho=
|
||||
xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
|
||||
|
|
|
@ -410,7 +410,7 @@ func (Check) GolangciFix() {
|
|||
runAndStreamOutput("golangci-lint", "run", "--fix")
|
||||
}
|
||||
|
||||
// Runs fmt-check, lint, got-swag, misspell-check, ineffasign-check, gocyclo-check, static-check, gosec-check, goconst-check all in parallel
|
||||
// Runs golangci and the swagger test in parralel
|
||||
func (Check) All() {
|
||||
mg.Deps(initVars)
|
||||
mg.Deps(
|
||||
|
|
|
@ -8,6 +8,8 @@ homepage: "https://vikunja.io"
|
|||
section: "default"
|
||||
priority: "extra"
|
||||
license: "AGPLv3"
|
||||
depends:
|
||||
- systemd
|
||||
contents:
|
||||
- src: <binlocation>
|
||||
dst: /opt/vikunja/vikunja
|
||||
|
@ -17,5 +19,7 @@ contents:
|
|||
- src: /opt/vikunja/vikunja
|
||||
dst: /usr/local/bin/vikunja
|
||||
type: "symlink"
|
||||
- src: vikunja.service
|
||||
dst: /usr/lib/systemd/system/vikunja.service
|
||||
scripts:
|
||||
postinstall: ./build/after-install.sh
|
||||
|
|
|
@ -120,7 +120,7 @@ var userListCmd = &cobra.Command{
|
|||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
users, err := user.ListUsers(s, "")
|
||||
users, err := user.ListAllUsers(s)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Error getting users: %s", err)
|
||||
|
|
|
@ -16,12 +16,14 @@
|
|||
title: testbucket3
|
||||
list_id: 1
|
||||
created_by_id: 1
|
||||
is_done_bucket: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 4
|
||||
title: testbucket4 - other list
|
||||
list_id: 2
|
||||
created_by_id: 1
|
||||
is_done_bucket: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
# The following are not or only partly owned by user 1
|
||||
|
@ -207,3 +209,9 @@
|
|||
created_by_id: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 35
|
||||
title: testbucket35
|
||||
list_id: 23
|
||||
created_by_id: -2
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
|
|
|
@ -9,3 +9,8 @@
|
|||
file_id: 9999
|
||||
created_by_id: 1
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 3
|
||||
task_id: 1
|
||||
file_id: 1
|
||||
created_by_id: -2
|
||||
created: 2018-12-01 15:13:12
|
||||
|
|
|
@ -94,3 +94,9 @@
|
|||
task_id: 36
|
||||
created: 2020-02-19 18:07:06
|
||||
updated: 2020-02-19 18:07:06
|
||||
- id: 17
|
||||
comment: comment 17
|
||||
author_id: -2
|
||||
task_id: 35
|
||||
created: 2020-02-19 18:07:06
|
||||
updated: 2020-02-19 18:07:06
|
||||
|
|
|
@ -338,5 +338,11 @@
|
|||
bucket_id: 20
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
||||
|
||||
- id: 37
|
||||
title: 'task #37'
|
||||
done: false
|
||||
created_by_id: -2
|
||||
list_id: 2
|
||||
index: 2
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
email: 'user7@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
discoverable_by_email: true
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
|
@ -86,6 +87,7 @@
|
|||
created: 2018-12-01 15:13:12
|
||||
- id: 11
|
||||
username: 'user11'
|
||||
name: 'Some one else'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user11@example.com'
|
||||
is_active: true
|
||||
|
@ -94,10 +96,12 @@
|
|||
created: 2018-12-01 15:13:12
|
||||
- id: 12
|
||||
username: 'user12'
|
||||
name: 'Name with spaces'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user12@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
discoverable_by_name: true
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 13
|
||||
|
|
|
@ -49,16 +49,6 @@ func InitEvents() (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
router.AddMiddleware(
|
||||
middleware.Retry{
|
||||
MaxRetries: 5,
|
||||
InitialInterval: time.Millisecond * 100,
|
||||
Logger: logger,
|
||||
Multiplier: 2,
|
||||
}.Middleware,
|
||||
middleware.Recoverer,
|
||||
)
|
||||
|
||||
metricsBuilder := metrics.NewPrometheusMetricsBuilder(vmetrics.GetRegistry(), "", "")
|
||||
metricsBuilder.AddPrometheusRouterMetrics(router)
|
||||
|
||||
|
@ -69,6 +59,33 @@ func InitEvents() (err error) {
|
|||
logger,
|
||||
)
|
||||
|
||||
poison, err := middleware.PoisonQueue(pubsub, "poison")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
router.AddNoPublisherHandler("poison.logger", "poison", pubsub, func(msg *message.Message) error {
|
||||
meta := ""
|
||||
for s, m := range msg.Metadata {
|
||||
meta += s + "=" + m + ", "
|
||||
}
|
||||
log.Errorf("Error while handling message %s, %s payload=%s", msg.UUID, meta, string(msg.Payload))
|
||||
return nil
|
||||
})
|
||||
|
||||
router.AddMiddleware(
|
||||
poison,
|
||||
middleware.Retry{
|
||||
MaxRetries: 5,
|
||||
InitialInterval: time.Millisecond * 100,
|
||||
MaxInterval: time.Hour,
|
||||
Multiplier: 2,
|
||||
MaxElapsedTime: 0,
|
||||
RandomizationFactor: 1,
|
||||
Logger: logger,
|
||||
}.Middleware,
|
||||
middleware.Recoverer,
|
||||
)
|
||||
|
||||
for topic, funcs := range listeners {
|
||||
for _, handler := range funcs {
|
||||
router.AddNoPublisherHandler(topic+"."+handler.Name(), topic, pubsub, handler.Handle)
|
||||
|
|
|
@ -19,6 +19,8 @@ package integrations
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -33,6 +35,20 @@ func TestBucket(t *testing.T) {
|
|||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerLinkShareWrite := webHandlerTest{
|
||||
linkShare: &models.LinkSharing{
|
||||
ID: 2,
|
||||
Hash: "test2",
|
||||
ListID: 2,
|
||||
Right: models.RightWrite,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
},
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.Bucket{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(nil, map[string]string{"list": "1"})
|
||||
|
@ -297,5 +313,15 @@ func TestBucket(t *testing.T) {
|
|||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Link Share", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
db.AssertExists(t, "buckets", map[string]interface{}{
|
||||
"list_id": 2,
|
||||
"created_by_id": -2,
|
||||
"title": "Lorem Ipsum",
|
||||
}, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -553,7 +553,7 @@ func TestLinkSharing(t *testing.T) {
|
|||
rec, err := testHandlerTaskWriteCollection.testReadAllWithLinkShare(nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3"`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #6`)
|
||||
|
|
|
@ -19,6 +19,8 @@ package integrations
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -33,6 +35,20 @@ func TestTaskComments(t *testing.T) {
|
|||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerLinkShareWrite := webHandlerTest{
|
||||
linkShare: &models.LinkSharing{
|
||||
ID: 2,
|
||||
Hash: "test2",
|
||||
ListID: 2,
|
||||
Right: models.RightWrite,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
},
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.TaskComment{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
// Only run specific nested tests:
|
||||
// ^TestTaskComments$/^Update$/^Update_task_items$/^Removing_Assignees_null$
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
|
@ -281,5 +297,15 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Link Share", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"task": "13"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
db.AssertExists(t, "task_comments", map[string]interface{}{
|
||||
"task_id": 13,
|
||||
"comment": "Lorem Ipsum",
|
||||
"author_id": -2,
|
||||
}, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ package integrations
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -33,6 +35,20 @@ func TestTask(t *testing.T) {
|
|||
},
|
||||
t: t,
|
||||
}
|
||||
testHandlerLinkShareWrite := webHandlerTest{
|
||||
linkShare: &models.LinkSharing{
|
||||
ID: 2,
|
||||
Hash: "test2",
|
||||
ListID: 2,
|
||||
Right: models.RightWrite,
|
||||
SharingType: models.SharingTypeWithoutPassword,
|
||||
SharedByID: 1,
|
||||
},
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.Task{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
// Only run specific nested tests:
|
||||
// ^TestTask$/^Update$/^Update_task_items$/^Removing_Assignees_null$
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
|
@ -489,5 +505,15 @@ func TestTask(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||
})
|
||||
})
|
||||
t.Run("Link Share", func(t *testing.T) {
|
||||
rec, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"list_id": 2,
|
||||
"title": "Lorem Ipsum",
|
||||
"created_by_id": -2,
|
||||
}, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -28,11 +28,7 @@ func TestUserList(t *testing.T) {
|
|||
t.Run("Normal test", func(t *testing.T) {
|
||||
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `user1`)
|
||||
assert.Contains(t, rec.Body.String(), `user2`)
|
||||
assert.Contains(t, rec.Body.String(), `user3`)
|
||||
assert.Contains(t, rec.Body.String(), `user4`)
|
||||
assert.Contains(t, rec.Body.String(), `user5`)
|
||||
assert.Equal(t, "null\n", rec.Body.String())
|
||||
})
|
||||
t.Run("Search for user3", func(t *testing.T) {
|
||||
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", map[string][]string{"s": {"user3"}}, nil)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type buckets20210321185225 struct {
|
||||
IsDoneBucket bool `xorm:"BOOL" json:"is_done_column"`
|
||||
}
|
||||
|
||||
func (buckets20210321185225) TableName() string {
|
||||
return "buckets"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210321185225",
|
||||
Description: "Add is done bucket to buckets",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(buckets20210321185225{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210328191017",
|
||||
Description: "Make sure all tables are correctly pluralized",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
// old name => new name
|
||||
tables := map[string]string{
|
||||
"label_task": "label_tasks",
|
||||
"link_sharing": "link_shares",
|
||||
"list": "lists",
|
||||
"team_list": "team_lists",
|
||||
"users_list": "users_lists",
|
||||
"users_namespace": "users_namespaces",
|
||||
}
|
||||
|
||||
for oldName, newName := range tables {
|
||||
err := renameTable(tx, oldName, newName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type savedFilters20210403145503 struct {
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
}
|
||||
|
||||
func (savedFilters20210403145503) TableName() string {
|
||||
return "saved_filters"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210403145503",
|
||||
Description: "",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(savedFilters20210403145503{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type linkShares20210403220653 struct {
|
||||
Name string `xorm:"text null" json:"name"`
|
||||
}
|
||||
|
||||
func (linkShares20210403220653) TableName() string {
|
||||
return "link_shares"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210403220653",
|
||||
Description: "Add the name column to link shares",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(linkShares20210403220653{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type users20210407170753 struct {
|
||||
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
|
||||
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210407170753) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210407170753",
|
||||
Description: "Add discoverable by email or name columns",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210407170753{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -158,6 +158,29 @@ func modifyColumn(x *xorm.Engine, tableName, col, newDefinition string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func renameTable(x *xorm.Engine, oldName, newName string) error {
|
||||
switch config.DatabaseType.GetString() {
|
||||
case "sqlite":
|
||||
_, err := x.Exec("ALTER TABLE `" + oldName + "` RENAME TO `" + newName + "`")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "mysql":
|
||||
_, err := x.Exec("RENAME TABLE `" + oldName + "` TO `" + newName + "`")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "postgres":
|
||||
_, err := x.Exec("ALTER TABLE `" + oldName + "` RENAME TO `" + newName + "`")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
log.Fatal("Unknown db.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSchema(tx *xorm.Engine) error {
|
||||
schemeBeans := []interface{}{}
|
||||
schemeBeans = append(schemeBeans, models.GetTables()...)
|
||||
|
|
|
@ -252,7 +252,7 @@ const ErrCodeListIsArchived = 3008
|
|||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrListIsArchived) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeListIsArchived, Message: "This lists is archived. Editing or creating new tasks is not possible."}
|
||||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeListIsArchived, Message: "This list is archived. Editing or creating new tasks is not possible."}
|
||||
}
|
||||
|
||||
// ================
|
||||
|
@ -1365,6 +1365,35 @@ func (err ErrBucketLimitExceeded) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrOnlyOneDoneBucketPerList represents an error where a bucket is set to the done bucket but one already exists for its list.
|
||||
type ErrOnlyOneDoneBucketPerList struct {
|
||||
BucketID int64
|
||||
ListID int64
|
||||
DoneBucketID int64
|
||||
}
|
||||
|
||||
// IsErrOnlyOneDoneBucketPerList checks if an error is ErrBucketLimitExceeded.
|
||||
func IsErrOnlyOneDoneBucketPerList(err error) bool {
|
||||
_, ok := err.(*ErrOnlyOneDoneBucketPerList)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrOnlyOneDoneBucketPerList) Error() string {
|
||||
return fmt.Sprintf("There can be only one done bucket per list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
|
||||
}
|
||||
|
||||
// ErrCodeOnlyOneDoneBucketPerList holds the unique world-error code of this error
|
||||
const ErrCodeOnlyOneDoneBucketPerList = 10005
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrOnlyOneDoneBucketPerList) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeOnlyOneDoneBucketPerList,
|
||||
Message: "There can be only one done bucket per list.",
|
||||
}
|
||||
}
|
||||
|
||||
// =============
|
||||
// Saved Filters
|
||||
// =============
|
||||
|
|
|
@ -38,6 +38,8 @@ type Bucket struct {
|
|||
|
||||
// How many tasks can be at the same time on this board max
|
||||
Limit int64 `xorm:"default 0" json:"limit"`
|
||||
// If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.
|
||||
IsDoneBucket bool `xorm:"BOOL" json:"is_done_bucket"`
|
||||
|
||||
// A timestamp when this bucket was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
|
@ -81,6 +83,21 @@ func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error)
|
|||
return
|
||||
}
|
||||
|
||||
func getDoneBucketForList(s *xorm.Session, listID int64) (bucket *Bucket, err error) {
|
||||
bucket = &Bucket{}
|
||||
exists, err := s.
|
||||
Where("list_id = ? and is_done_bucket = ?", listID, true).
|
||||
Get(bucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
bucket = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll returns all buckets with their tasks for a certain list
|
||||
// @Summary Get all kanban buckets of a list
|
||||
// @Description Returns all kanban buckets with belong to a list including their tasks.
|
||||
|
@ -118,12 +135,9 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
}
|
||||
|
||||
// Get all users
|
||||
users := make(map[int64]*user.User)
|
||||
if len(userIDs) > 0 {
|
||||
err = s.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
users, err := getUsersOrLinkSharesFromIDs(s, userIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, bb := range buckets {
|
||||
|
@ -217,7 +231,11 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/buckets [put]
|
||||
func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
b.CreatedByID = a.GetID()
|
||||
b.CreatedBy, err = GetUserOrLinkShareUser(s, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b.CreatedByID = b.CreatedBy.ID
|
||||
|
||||
_, err = s.Insert(b)
|
||||
return
|
||||
|
@ -239,9 +257,26 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/buckets/{bucketID} [post]
|
||||
func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
doneBucket, err := getDoneBucketForList(s, b.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if doneBucket != nil && doneBucket.IsDoneBucket && b.IsDoneBucket {
|
||||
return &ErrOnlyOneDoneBucketPerList{
|
||||
BucketID: b.ID,
|
||||
ListID: b.ListID,
|
||||
DoneBucketID: doneBucket.ID,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("id = ?", b.ID).
|
||||
Cols("title", "limit").
|
||||
Cols(
|
||||
"title",
|
||||
"limit",
|
||||
"is_done_bucket",
|
||||
).
|
||||
Update(b)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -89,6 +89,20 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
assert.Len(t, buckets, 3)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[0].ID)
|
||||
})
|
||||
t.Run("link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
testuser := &user.User{ID: 1}
|
||||
b := &Bucket{ListID: 23}
|
||||
result, _, _, err := b.ReadAll(s, testuser, "", 0, 0)
|
||||
assert.NoError(t, err)
|
||||
buckets, _ := result.([]*Bucket)
|
||||
assert.Len(t, buckets, 1)
|
||||
assert.NotNil(t, buckets[0].CreatedBy)
|
||||
assert.Equal(t, int64(-2), buckets[0].CreatedByID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBucket_Delete(t *testing.T) {
|
||||
|
@ -182,4 +196,19 @@ func TestBucket_Update(t *testing.T) {
|
|||
|
||||
testAndAssertBucketUpdate(t, b, s)
|
||||
})
|
||||
t.Run("only one done bucket per list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
b := &Bucket{
|
||||
ID: 1,
|
||||
ListID: 1,
|
||||
IsDoneBucket: true,
|
||||
}
|
||||
|
||||
err := b.Update(s, &user.User{ID: 1})
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrOnlyOneDoneBucketPerList(err))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -146,10 +146,7 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
|
|||
return nil, 0, 0, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
u := &user.User{ID: a.GetID()}
|
||||
|
||||
// Get all tasks
|
||||
taskIDs, err := getUserTaskIDs(s, u)
|
||||
u, err := user.GetUserByID(s, a.GetID())
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -157,7 +154,7 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
|
|||
return getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
Search: search,
|
||||
User: u,
|
||||
TaskIDs: taskIDs,
|
||||
GetForUser: u.ID,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
GetUnusedLabels: true,
|
||||
|
@ -206,34 +203,3 @@ func getLabelByIDSimple(s *xorm.Session, labelID int64) (*Label, error) {
|
|||
}
|
||||
return &label, err
|
||||
}
|
||||
|
||||
// Helper method to get all task ids a user has
|
||||
func getUserTaskIDs(s *xorm.Session, u *user.User) (taskIDs []int64, err error) {
|
||||
|
||||
// Get all lists
|
||||
lists, _, _, err := getRawListsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
user: u,
|
||||
page: -1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tasks, _, _, err := getRawTasksForLists(s, lists, u, &taskOptions{
|
||||
page: -1,
|
||||
perPage: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make a slice of task ids
|
||||
for _, t := range tasks {
|
||||
taskIDs = append(taskIDs, t.ID)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -64,21 +64,28 @@ func (l *Label) isLabelOwner(s *xorm.Session, a web.Auth) (bool, error) {
|
|||
// Helper method to check if a user can see a specific label
|
||||
func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRight int, err error) {
|
||||
|
||||
// TODO: add an extra check for link share handling
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
// Get all tasks
|
||||
taskIDs, err := getUserTaskIDs(s, &user.User{ID: a.GetID()})
|
||||
u, err := user.GetUserByID(s, a.GetID())
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
|
||||
// Get all labels associated with these tasks
|
||||
cond := builder.In("label_tasks.task_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("tasks").
|
||||
Where(builder.In("list_id", getUserListsStatement(u.ID).Select("l.id"))),
|
||||
)
|
||||
|
||||
ll := &LabelTask{}
|
||||
has, err = s.Table("labels").
|
||||
Select("label_task.*").
|
||||
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
||||
Where("label_task.label_id is not null OR labels.created_by_id = ?", a.GetID()).
|
||||
Or(builder.In("label_task.task_id", taskIDs)).
|
||||
Select("label_tasks.*").
|
||||
Join("LEFT", "label_tasks", "label_tasks.label_id = labels.id").
|
||||
Where("label_tasks.label_id is not null OR labels.created_by_id = ?", u.ID).
|
||||
Or(cond).
|
||||
And("labels.id = ?", l.ID).
|
||||
Exist(ll)
|
||||
if err != nil {
|
||||
|
|
|
@ -44,7 +44,7 @@ type LabelTask struct {
|
|||
|
||||
// TableName makes a pretty table name
|
||||
func (LabelTask) TableName() string {
|
||||
return "label_task"
|
||||
return "label_tasks"
|
||||
}
|
||||
|
||||
// Delete deletes a label on a task
|
||||
|
@ -149,6 +149,7 @@ type LabelByTaskIDsOptions struct {
|
|||
TaskIDs []int64
|
||||
GetUnusedLabels bool
|
||||
GroupByLabelIDsOnly bool
|
||||
GetForUser int64
|
||||
}
|
||||
|
||||
// Helper function to get all labels for a set of tasks
|
||||
|
@ -158,8 +159,8 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
|||
// multiple times when it is associated to more than one task.
|
||||
// Because of this whole thing, we need this extra switch here to only group by Task IDs if needed.
|
||||
// Probably not the most ideal solution.
|
||||
var groupBy = "labels.id,label_task.task_id"
|
||||
var selectStmt = "labels.*, label_task.task_id"
|
||||
var groupBy = "labels.id,label_tasks.task_id"
|
||||
var selectStmt = "labels.*, label_tasks.task_id"
|
||||
if opts.GroupByLabelIDsOnly {
|
||||
groupBy = "labels.id"
|
||||
selectStmt = "labels.*"
|
||||
|
@ -167,23 +168,33 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
|||
|
||||
// Get all labels associated with these tasks
|
||||
var labels []*labelWithTaskID
|
||||
cond := builder.And(builder.NotNull{"label_task.label_id"})
|
||||
if len(opts.TaskIDs) > 0 {
|
||||
cond = builder.And(builder.In("label_task.task_id", opts.TaskIDs), cond)
|
||||
cond := builder.And(builder.NotNull{"label_tasks.label_id"})
|
||||
if len(opts.TaskIDs) > 0 && opts.GetForUser == 0 {
|
||||
cond = builder.And(builder.In("label_tasks.task_id", opts.TaskIDs), cond)
|
||||
}
|
||||
if opts.GetForUser != 0 {
|
||||
cond = builder.And(builder.In("label_tasks.task_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("tasks").
|
||||
Where(builder.In("list_id", getUserListsStatement(opts.GetForUser).Select("l.id"))),
|
||||
), cond)
|
||||
}
|
||||
if opts.GetUnusedLabels {
|
||||
cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.ID})
|
||||
}
|
||||
|
||||
vals := strings.Split(opts.Search, ",")
|
||||
ids := []int64{}
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("Label search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
if opts.Search != "" {
|
||||
vals := strings.Split(opts.Search, ",")
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("Label search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
|
@ -196,7 +207,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
|||
|
||||
query := s.Table("labels").
|
||||
Select(selectStmt).
|
||||
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
||||
Join("LEFT", "label_tasks", "label_tasks.label_id = labels.id").
|
||||
Where(cond).
|
||||
GroupBy(groupBy).
|
||||
OrderBy("labels.id ASC")
|
||||
|
@ -238,7 +249,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
|||
// Get the total number of entries
|
||||
totalEntries, err = s.Table("labels").
|
||||
Select("count(DISTINCT labels.id)").
|
||||
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
||||
Join("LEFT", "label_tasks", "label_tasks.label_id = labels.id").
|
||||
Where(cond).
|
||||
And("labels.title LIKE ?", "%"+opts.Search+"%").
|
||||
Count(&Label{})
|
||||
|
|
|
@ -215,11 +215,11 @@ func TestLabelTask_Create(t *testing.T) {
|
|||
CRUDable: tt.fields.CRUDable,
|
||||
Rights: tt.fields.Rights,
|
||||
}
|
||||
allowed, _ := l.CanCreate(s, tt.args.a)
|
||||
allowed, err := l.CanCreate(s, tt.args.a)
|
||||
if !allowed && !tt.wantForbidden {
|
||||
t.Errorf("LabelTask.CanCreate() forbidden, want %v", tt.wantForbidden)
|
||||
t.Errorf("LabelTask.CanCreate() forbidden, want %v, err %v", tt.wantForbidden, err)
|
||||
}
|
||||
err := l.Create(s, tt.args.a)
|
||||
err = l.Create(s, tt.args.a)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("LabelTask.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
@ -227,7 +227,7 @@ func TestLabelTask_Create(t *testing.T) {
|
|||
t.Errorf("LabelTask.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "label_task", map[string]interface{}{
|
||||
db.AssertExists(t, "label_tasks", map[string]interface{}{
|
||||
"id": l.ID,
|
||||
"task_id": l.TaskID,
|
||||
"label_id": l.LabelID,
|
||||
|
@ -326,7 +326,7 @@ func TestLabelTask_Delete(t *testing.T) {
|
|||
t.Errorf("LabelTask.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
|
||||
}
|
||||
if !tt.wantForbidden {
|
||||
db.AssertMissing(t, "label_task", map[string]interface{}{
|
||||
db.AssertMissing(t, "label_tasks", map[string]interface{}{
|
||||
"label_id": l.LabelID,
|
||||
"task_id": l.TaskID,
|
||||
})
|
||||
|
|
|
@ -42,6 +42,8 @@ type LinkSharing struct {
|
|||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"share"`
|
||||
// The public id to get this shared list
|
||||
Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"`
|
||||
// The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.
|
||||
Name string `xorm:"text null" json:"name"`
|
||||
// The ID of the shared list
|
||||
ListID int64 `xorm:"bigint not null" json:"-" param:"list"`
|
||||
// The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
|
@ -65,7 +67,7 @@ type LinkSharing struct {
|
|||
|
||||
// TableName holds the table name
|
||||
func (LinkSharing) TableName() string {
|
||||
return "link_sharing"
|
||||
return "link_shares"
|
||||
}
|
||||
|
||||
// GetID returns the ID of the links sharing object
|
||||
|
@ -84,6 +86,25 @@ func GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error
|
|||
return
|
||||
}
|
||||
|
||||
func (share *LinkSharing) getUserID() int64 {
|
||||
return share.ID * -1
|
||||
}
|
||||
|
||||
func (share *LinkSharing) toUser() *user.User {
|
||||
suffix := "Link Share"
|
||||
if share.Name != "" {
|
||||
suffix = " (" + suffix + ")"
|
||||
}
|
||||
|
||||
return &user.User{
|
||||
ID: share.getUserID(),
|
||||
Name: share.Name + suffix,
|
||||
Username: share.Name,
|
||||
Created: share.Created,
|
||||
Updated: share.Updated,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new link share for a given list
|
||||
// @Summary Share a list via link
|
||||
// @Description Share a list via link. The user needs to have write-access to the list to be able do this.
|
||||
|
@ -246,3 +267,23 @@ func GetListByShareHash(s *xorm.Session, hash string) (list *List, err error) {
|
|||
list, err = GetListSimpleByID(s, share.ListID)
|
||||
return
|
||||
}
|
||||
|
||||
// GetLinkShareByID returns a link share by its id.
|
||||
func GetLinkShareByID(s *xorm.Session, id int64) (share *LinkSharing, err error) {
|
||||
share = &LinkSharing{}
|
||||
has, err := s.Where("id = ?", id).Get(share)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
return share, ErrListShareDoesNotExist{ID: id}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetLinkSharesByIDs returns all link shares from a slice of ids
|
||||
func GetLinkSharesByIDs(s *xorm.Session, ids []int64) (shares map[int64]*LinkSharing, err error) {
|
||||
shares = make(map[int64]*LinkSharing)
|
||||
err = s.In("id", ids).Find(&shares)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -21,11 +21,10 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
|
@ -81,6 +80,11 @@ type List struct {
|
|||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName returns a better name for the lists table
|
||||
func (l *List) TableName() string {
|
||||
return "lists"
|
||||
}
|
||||
|
||||
// ListBackgroundType holds a list background type
|
||||
type ListBackgroundType struct {
|
||||
Type string
|
||||
|
@ -102,20 +106,42 @@ var FavoritesPseudoList = List{
|
|||
|
||||
// GetListsByNamespaceID gets all lists in a namespace
|
||||
func GetListsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (lists []*List, err error) {
|
||||
if nID == -1 {
|
||||
err = s.Select("l.*").
|
||||
Table("list").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Where("tm.user_id = ?", doer.ID).
|
||||
Where("l.is_archived = false").
|
||||
Where("n.is_archived = false").
|
||||
Or("ul.user_id = ?", doer.ID).
|
||||
GroupBy("l.id").
|
||||
Find(&lists)
|
||||
} else {
|
||||
switch nID {
|
||||
case SharedListsPseudoNamespace.ID:
|
||||
nnn, err := getSharedListsInNamespace(s, false, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Lists != nil {
|
||||
lists = nnn.Lists
|
||||
}
|
||||
case FavoritesPseudoNamespace.ID:
|
||||
namespaces := make(map[int64]*NamespaceWithLists)
|
||||
_, err := getNamespacesWithLists(s, &namespaces, "", false, 0, -1, doer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
namespaceIDs, _ := getNamespaceOwnerIDs(namespaces)
|
||||
ls, err := getListsForNamespaces(s, namespaceIDs, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nnn, err := getFavoriteLists(s, ls, namespaceIDs, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Lists != nil {
|
||||
lists = nnn.Lists
|
||||
}
|
||||
case SavedFiltersPseudoNamespace.ID:
|
||||
nnn, err := getSavedFilters(s, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nnn != nil && nnn.Lists != nil {
|
||||
lists = nnn.Lists
|
||||
}
|
||||
default:
|
||||
err = s.Select("l.*").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
|
@ -271,9 +297,9 @@ func GetListSimplByTaskID(s *xorm.Session, taskID int64) (l *List, err error) {
|
|||
// leading to not finding anything if the id is good, but for example the title is different.
|
||||
var list List
|
||||
exists, err := s.
|
||||
Select("list.*").
|
||||
Select("lists.*").
|
||||
Table(List{}).
|
||||
Join("INNER", "tasks", "list.id = tasks.list_id").
|
||||
Join("INNER", "tasks", "lists.id = tasks.list_id").
|
||||
Where("tasks.id = ?", taskID).
|
||||
Get(&list)
|
||||
if err != nil {
|
||||
|
@ -307,6 +333,32 @@ type listOptions struct {
|
|||
isArchived bool
|
||||
}
|
||||
|
||||
func getUserListsStatement(userID int64) *builder.Builder {
|
||||
dialect := config.DatabaseType.GetString()
|
||||
if dialect == "sqlite" {
|
||||
dialect = builder.SQLITE
|
||||
}
|
||||
|
||||
return builder.Dialect(dialect).
|
||||
Select("l.*").
|
||||
From("lists", "l").
|
||||
Join("INNER", "namespaces n", "l.namespace_id = n.id").
|
||||
Join("LEFT", "team_namespaces tn", "tn.namespace_id = n.id").
|
||||
Join("LEFT", "team_members tm", "tm.team_id = tn.team_id").
|
||||
Join("LEFT", "team_lists tl", "l.id = tl.list_id").
|
||||
Join("LEFT", "team_members tm2", "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", "users_lists ul", "ul.list_id = l.id").
|
||||
Join("LEFT", "users_namespaces un", "un.namespace_id = l.namespace_id").
|
||||
Where(builder.Or(
|
||||
builder.Eq{"tm.user_id": userID},
|
||||
builder.Eq{"tm2.user_id": userID},
|
||||
builder.Eq{"ul.user_id": userID},
|
||||
builder.Eq{"un.user_id": userID},
|
||||
builder.Eq{"l.owner_id": userID},
|
||||
)).
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
||||
// Gets the lists only, without any tasks or so
|
||||
func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resultCount int, totalItems int64, err error) {
|
||||
fullUser, err := user.GetUserByID(s, opts.user.ID)
|
||||
|
@ -326,15 +378,17 @@ func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resu
|
|||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
|
||||
var filterCond builder.Cond
|
||||
vals := strings.Split(opts.search, ",")
|
||||
ids := []int64{}
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("List search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
if opts.search != "" {
|
||||
vals := strings.Split(opts.search, ",")
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("List search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
|
@ -345,54 +399,23 @@ func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resu
|
|||
|
||||
// Gets all Lists where the user is either owner or in a team which has access to the list
|
||||
// Or in a team which has namespace read access
|
||||
query := s.Select("l.*").
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("INNER", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, "tn.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Where(builder.Or(
|
||||
builder.Eq{"tm.user_id": fullUser.ID},
|
||||
builder.Eq{"tm2.user_id": fullUser.ID},
|
||||
builder.Eq{"ul.user_id": fullUser.ID},
|
||||
builder.Eq{"un.user_id": fullUser.ID},
|
||||
builder.Eq{"l.owner_id": fullUser.ID},
|
||||
)).
|
||||
GroupBy("l.id").
|
||||
|
||||
query := getUserListsStatement(fullUser.ID).
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
err = query.Find(&lists)
|
||||
err = s.SQL(query).Find(&lists)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
totalItems, err = s.
|
||||
Table("list").
|
||||
Alias("l").
|
||||
Join("INNER", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, "tn.namespace_id = n.id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Where(builder.Or(
|
||||
builder.Eq{"tm.user_id": fullUser.ID},
|
||||
builder.Eq{"tm2.user_id": fullUser.ID},
|
||||
builder.Eq{"ul.user_id": fullUser.ID},
|
||||
builder.Eq{"un.user_id": fullUser.ID},
|
||||
builder.Eq{"l.owner_id": fullUser.ID},
|
||||
)).
|
||||
GroupBy("l.id").
|
||||
query = getUserListsStatement(fullUser.ID).
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond).
|
||||
Where(isArchivedCond)
|
||||
totalItems, err = s.
|
||||
SQL(query.Select("count(*)")).
|
||||
Count(&List{})
|
||||
return lists, len(lists), totalItems, err
|
||||
}
|
||||
|
@ -470,9 +493,9 @@ func (l *List) CheckIsArchived(s *xorm.Session) (err error) {
|
|||
|
||||
nl := &NamespaceList{}
|
||||
exists, err := s.
|
||||
Table("list").
|
||||
Join("LEFT", "namespaces", "list.namespace_id = namespaces.id").
|
||||
Where("list.id = ? AND (list.is_archived = true OR namespaces.is_archived = true)", l.ID).
|
||||
Table("lists").
|
||||
Join("LEFT", "namespaces", "lists.namespace_id = namespaces.id").
|
||||
Where("lists.id = ? AND (lists.is_archived = true OR namespaces.is_archived = true)", l.ID).
|
||||
Get(nl)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -521,6 +544,7 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
|
|||
"identifier",
|
||||
"hex_color",
|
||||
"is_favorite",
|
||||
"background_file_id",
|
||||
}
|
||||
if list.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
|
@ -562,6 +586,25 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id} [post]
|
||||
func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
fid := getSavedFilterIDFromListID(l.ID)
|
||||
if fid > 0 {
|
||||
f, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.IsFavorite = l.IsFavorite
|
||||
f.Title = l.Title
|
||||
f.Description = l.Description
|
||||
err = f.Update(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = *f.toList()
|
||||
return nil
|
||||
}
|
||||
|
||||
err = CreateOrUpdateList(s, l, a)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -115,6 +115,17 @@ func (l *List) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error
|
|||
if l.ID == FavoritesPseudoList.ID {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fid := getSavedFilterIDFromListID(l.ID)
|
||||
if fid > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return sf.CanUpdate(s, a)
|
||||
}
|
||||
|
||||
canUpdate, err = l.CanWrite(s, a)
|
||||
// If the list is archived and the user tries to un-archive it, let the request through
|
||||
if IsErrListIsArchived(err) && !l.IsArchived {
|
||||
|
@ -222,16 +233,16 @@ func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, i
|
|||
r := &allListRights{}
|
||||
var maxRight = 0
|
||||
exists, err := s.
|
||||
Table("list").
|
||||
Table("lists").
|
||||
Alias("l").
|
||||
// User stuff
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_lists", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
|
||||
// Team stuff
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_lists", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
// The actual condition
|
||||
Where(builder.And(
|
||||
|
|
|
@ -47,7 +47,7 @@ type TeamList struct {
|
|||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamList) TableName() string {
|
||||
return "team_list"
|
||||
return "team_lists"
|
||||
}
|
||||
|
||||
// TeamWithRight represents a team, combined with rights.
|
||||
|
@ -196,8 +196,8 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
all := []*TeamWithRight{}
|
||||
query := s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_list", "team_id = teams.id").
|
||||
Where("team_list.list_id = ?", tl.ListID).
|
||||
Join("INNER", "team_lists", "team_id = teams.id").
|
||||
Where("team_lists.list_id = ?", tl.ListID).
|
||||
Where("teams.name LIKE ?", "%"+search+"%")
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
|
@ -219,8 +219,8 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
|
||||
totalItems, err = s.
|
||||
Table("teams").
|
||||
Join("INNER", "team_list", "team_id = teams.id").
|
||||
Where("team_list.list_id = ?", tl.ListID).
|
||||
Join("INNER", "team_lists", "team_id = teams.id").
|
||||
Where("team_lists.list_id = ?", tl.ListID).
|
||||
Where("teams.name LIKE ?", "%"+search+"%").
|
||||
Count(&TeamWithRight{})
|
||||
if err != nil {
|
||||
|
|
|
@ -99,7 +99,7 @@ func TestTeamList_Create(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "team_list", map[string]interface{}{
|
||||
db.AssertExists(t, "team_lists", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"list_id": 1,
|
||||
"right": RightAdmin,
|
||||
|
@ -171,7 +171,7 @@ func TestTeamList_Delete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "team_list", map[string]interface{}{
|
||||
db.AssertMissing(t, "team_lists", map[string]interface{}{
|
||||
"team_id": 1,
|
||||
"list_id": 3,
|
||||
})
|
||||
|
@ -279,7 +279,7 @@ func TestTeamList_Update(t *testing.T) {
|
|||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "team_list", map[string]interface{}{
|
||||
db.AssertExists(t, "team_lists", map[string]interface{}{
|
||||
"list_id": tt.fields.ListID,
|
||||
"team_id": tt.fields.TeamID,
|
||||
"right": tt.fields.Right,
|
||||
|
|
|
@ -45,7 +45,7 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "list", map[string]interface{}{
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
|
@ -105,7 +105,7 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "list", map[string]interface{}{
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
|
@ -129,7 +129,7 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "list", map[string]interface{}{
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
|
@ -176,7 +176,7 @@ func TestList_Delete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "list", map[string]interface{}{
|
||||
db.AssertMissing(t, "lists", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ type ListUser struct {
|
|||
|
||||
// TableName is the table name for ListUser
|
||||
func (ListUser) TableName() string {
|
||||
return "users_list"
|
||||
return "users_lists"
|
||||
}
|
||||
|
||||
// UserWithRight represents a user in combination with the right it can have on a list/namespace
|
||||
|
@ -202,8 +202,8 @@ func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
// Get all users
|
||||
all := []*UserWithRight{}
|
||||
query := s.
|
||||
Join("INNER", "users_list", "user_id = users.id").
|
||||
Where("users_list.list_id = ?", lu.ListID).
|
||||
Join("INNER", "users_lists", "user_id = users.id").
|
||||
Where("users_lists.list_id = ?", lu.ListID).
|
||||
Where("users.username LIKE ?", "%"+search+"%")
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
|
@ -219,8 +219,8 @@ func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Join("INNER", "users_list", "user_id = users.id").
|
||||
Where("users_list.list_id = ?", lu.ListID).
|
||||
Join("INNER", "users_lists", "user_id = users.id").
|
||||
Where("users_lists.list_id = ?", lu.ListID).
|
||||
Where("users.username LIKE ?", "%"+search+"%").
|
||||
Count(&UserWithRight{})
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ func TestListUser_Create(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_list", map[string]interface{}{
|
||||
db.AssertExists(t, "users_lists", map[string]interface{}{
|
||||
"user_id": ul.UserID,
|
||||
"list_id": tt.fields.ListID,
|
||||
}, false)
|
||||
|
@ -323,7 +323,7 @@ func TestListUser_Update(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_list", map[string]interface{}{
|
||||
db.AssertExists(t, "users_lists", map[string]interface{}{
|
||||
"list_id": tt.fields.ListID,
|
||||
"user_id": lu.UserID,
|
||||
"right": tt.fields.Right,
|
||||
|
@ -405,7 +405,7 @@ func TestListUser_Delete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertMissing(t, "users_list", map[string]interface{}{
|
||||
db.AssertMissing(t, "users_lists", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"list_id": tt.fields.ListID,
|
||||
})
|
||||
|
|
|
@ -113,6 +113,10 @@ func getNamespaceSimpleByID(s *xorm.Session, id int64) (namespace *Namespace, er
|
|||
return &FavoritesPseudoNamespace, nil
|
||||
}
|
||||
|
||||
if id == SavedFiltersPseudoNamespace.ID {
|
||||
return &SavedFiltersPseudoNamespace, nil
|
||||
}
|
||||
|
||||
namespace = &Namespace{}
|
||||
|
||||
exists, err := s.Where("id = ?", id).Get(namespace)
|
||||
|
@ -246,10 +250,10 @@ func getNamespacesWithLists(s *xorm.Session, namespaces *map[int64]*NamespaceWit
|
|||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", userID).
|
||||
Or("namespaces.owner_id = ?", userID).
|
||||
Or("users_namespace.user_id = ?", userID).
|
||||
Or("users_namespaces.user_id = ?", userID).
|
||||
GroupBy("namespaces.id").
|
||||
Where(filterCond).
|
||||
Where(isArchivedCond)
|
||||
|
@ -265,10 +269,10 @@ func getNamespacesWithLists(s *xorm.Session, namespaces *map[int64]*NamespaceWit
|
|||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
Where("team_members.user_id = ?", userID).
|
||||
Or("namespaces.owner_id = ?", userID).
|
||||
Or("users_namespace.user_id = ?", userID).
|
||||
Or("users_namespaces.user_id = ?", userID).
|
||||
And("namespaces.is_archived = false").
|
||||
GroupBy("namespaces.id").
|
||||
Where(filterCond).
|
||||
|
@ -328,11 +332,11 @@ func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User)
|
|||
// Get all lists individually shared with our user (not via a namespace)
|
||||
individualLists := []*List{}
|
||||
iListQuery := s.Select("l.*").
|
||||
Table("list").
|
||||
Table("lists").
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_lists", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tl.team_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"users_lists", "ul"}, "ul.list_id = l.id").
|
||||
Where(builder.And(
|
||||
builder.Eq{"tm.user_id": doer.ID},
|
||||
builder.Neq{"l.owner_id": doer.ID},
|
||||
|
@ -386,8 +390,8 @@ func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer
|
|||
// Check if we have any favorites or favorited lists and remove the favorites namespace from the list if not
|
||||
var favoriteCount int64
|
||||
favoriteCount, err = s.
|
||||
Join("INNER", "list", "tasks.list_id = list.id").
|
||||
Join("INNER", "namespaces", "list.namespace_id = namespaces.id").
|
||||
Join("INNER", "lists", "tasks.list_id = lists.id").
|
||||
Join("INNER", "namespaces", "lists.namespace_id = namespaces.id").
|
||||
Where(builder.And(builder.Eq{"tasks.is_favorite": true}, builder.In("namespaces.id", namespaceIDs))).
|
||||
Count(&Task{})
|
||||
if err != nil {
|
||||
|
@ -430,14 +434,10 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
|
|||
}
|
||||
|
||||
for _, filter := range savedFilters {
|
||||
savedFiltersNamespace.Lists = append(savedFiltersNamespace.Lists, &List{
|
||||
ID: getListIDFromSavedFilterID(filter.ID),
|
||||
Title: filter.Title,
|
||||
Description: filter.Description,
|
||||
Created: filter.Created,
|
||||
Updated: filter.Updated,
|
||||
Owner: doer,
|
||||
})
|
||||
filterList := filter.toList()
|
||||
filterList.NamespaceID = savedFiltersNamespace.ID
|
||||
filterList.Owner = doer
|
||||
savedFiltersNamespace.Lists = append(savedFiltersNamespace.Lists, filterList)
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -517,6 +517,19 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
lists = append(lists, sharedListsNamespace.Lists...)
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Saved Filters
|
||||
|
||||
savedFiltersNamespace, err := getSavedFilters(s, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if savedFiltersNamespace != nil {
|
||||
namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
|
||||
lists = append(lists, savedFiltersNamespace.Lists...)
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Favorite lists
|
||||
|
||||
|
@ -529,18 +542,6 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
namespaces[favoritesNamespace.ID] = favoritesNamespace
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Saved Filters
|
||||
|
||||
savedFiltersNamespace, err := getSavedFilters(s, doer)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if savedFiltersNamespace != nil {
|
||||
namespaces[savedFiltersNamespace.ID] = savedFiltersNamespace
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Put it all together
|
||||
|
||||
|
@ -550,6 +551,10 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
}
|
||||
|
||||
for _, list := range lists {
|
||||
if list.NamespaceID == SharedListsPseudoNamespace.ID || list.NamespaceID == SavedFiltersPseudoNamespace.ID {
|
||||
// Shared lists and filtered lists are already in the namespace
|
||||
continue
|
||||
}
|
||||
namespaces[list.NamespaceID].Lists = append(namespaces[list.NamespaceID].Lists, list)
|
||||
}
|
||||
|
||||
|
|
|
@ -72,7 +72,10 @@ func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bo
|
|||
return false, 0, err
|
||||
}
|
||||
|
||||
if a.GetID() == nn.OwnerID {
|
||||
if a.GetID() == nn.OwnerID ||
|
||||
nn.ID == SharedListsPseudoNamespace.ID ||
|
||||
nn.ID == FavoritesPseudoNamespace.ID ||
|
||||
nn.ID == SavedFiltersPseudoNamespace.ID {
|
||||
return true, int(RightAdmin), nil
|
||||
}
|
||||
|
||||
|
@ -80,7 +83,7 @@ func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bo
|
|||
The following loop creates an sql condition like this one:
|
||||
|
||||
namespaces.owner_id = 1 OR
|
||||
(users_namespace.user_id = 1 AND users_namespace.right = 1) OR
|
||||
(users_namespaces.user_id = 1 AND users_namespaces.right = 1) OR
|
||||
(team_members.user_id = 1 AND team_namespaces.right = 1) OR
|
||||
|
||||
|
||||
|
@ -94,8 +97,8 @@ func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bo
|
|||
// User conditions
|
||||
// If the namespace was shared directly with the user and the user has the right
|
||||
conds = append(conds, builder.And(
|
||||
builder.Eq{"users_namespace.user_id": a.GetID()},
|
||||
builder.Eq{"users_namespace.right": r},
|
||||
builder.Eq{"users_namespaces.user_id": a.GetID()},
|
||||
builder.Eq{"users_namespaces.right": r},
|
||||
))
|
||||
|
||||
// Team rights
|
||||
|
@ -117,7 +120,7 @@ func (n *Namespace) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bo
|
|||
Select("*").
|
||||
Table("namespaces").
|
||||
// User stuff
|
||||
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Join("LEFT", "users_namespaces", "users_namespaces.namespace_id = namespaces.id").
|
||||
// Teams stuff
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
|
|
|
@ -194,6 +194,7 @@ func TestNamespace_Delete(t *testing.T) {
|
|||
|
||||
func TestNamespace_ReadAll(t *testing.T) {
|
||||
user1 := &user.User{ID: 1}
|
||||
user6 := &user.User{ID: 6}
|
||||
user7 := &user.User{ID: 7}
|
||||
user11 := &user.User{ID: 11}
|
||||
user12 := &user.User{ID: 12}
|
||||
|
@ -209,7 +210,7 @@ func TestNamespace_ReadAll(t *testing.T) {
|
|||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Len(t, namespaces, 11) // Total of 11 including shared, favorites and saved filters
|
||||
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with shared filters
|
||||
assert.Equal(t, int64(-3), namespaces[0].ID) // The first one should be the one with saved filters
|
||||
assert.Equal(t, int64(-2), namespaces[1].ID) // The second one should be the one with favorites
|
||||
assert.Equal(t, int64(-1), namespaces[2].ID) // The third one should be the one with the shared namespaces
|
||||
// Ensure every list and namespace are not archived
|
||||
|
@ -220,6 +221,28 @@ func TestNamespace_ReadAll(t *testing.T) {
|
|||
}
|
||||
}
|
||||
})
|
||||
t.Run("no own shared lists", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
n := &Namespace{}
|
||||
nn, _, _, err := n.ReadAll(s, user6, "", 1, -1)
|
||||
assert.NoError(t, err)
|
||||
namespaces := nn.([]*NamespaceWithLists)
|
||||
assert.NotNil(t, namespaces)
|
||||
assert.Equal(t, int64(-1), namespaces[1].ID) // The third one should be the one with the shared namespaces
|
||||
|
||||
sharedListOccurences := make(map[int64]int64)
|
||||
for _, list := range namespaces[1].Lists {
|
||||
assert.NotEqual(t, user1.ID, list.OwnerID)
|
||||
sharedListOccurences[list.ID]++
|
||||
}
|
||||
|
||||
for listID, occ := range sharedListOccurences {
|
||||
assert.Equal(t, int64(1), occ, "shared list %d is present %d times, should be 1", listID, occ)
|
||||
}
|
||||
})
|
||||
t.Run("namespaces only", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
|
|
|
@ -49,7 +49,7 @@ type NamespaceUser struct {
|
|||
|
||||
// TableName is the table name for NamespaceUser
|
||||
func (NamespaceUser) TableName() string {
|
||||
return "users_namespace"
|
||||
return "users_namespaces"
|
||||
}
|
||||
|
||||
// Create creates a new namespace <-> user relation
|
||||
|
@ -189,8 +189,8 @@ func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, pag
|
|||
limit, start := getLimitFromPageIndex(page, perPage)
|
||||
|
||||
query := s.
|
||||
Join("INNER", "users_namespace", "user_id = users.id").
|
||||
Where("users_namespace.namespace_id = ?", nu.NamespaceID).
|
||||
Join("INNER", "users_namespaces", "user_id = users.id").
|
||||
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
|
||||
Where("users.username LIKE ?", "%"+search+"%")
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
|
@ -206,8 +206,8 @@ func (nu *NamespaceUser) ReadAll(s *xorm.Session, a web.Auth, search string, pag
|
|||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
Join("INNER", "users_namespace", "user_id = users.id").
|
||||
Where("users_namespace.namespace_id = ?", nu.NamespaceID).
|
||||
Join("INNER", "users_namespaces", "user_id = users.id").
|
||||
Where("users_namespaces.namespace_id = ?", nu.NamespaceID).
|
||||
Where("users.username LIKE ?", "%"+search+"%").
|
||||
Count(&UserWithRight{})
|
||||
|
||||
|
|
|
@ -132,7 +132,7 @@ func TestNamespaceUser_Create(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_namespace", map[string]interface{}{
|
||||
db.AssertExists(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
}, false)
|
||||
|
@ -326,7 +326,7 @@ func TestNamespaceUser_Update(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertExists(t, "users_namespace", map[string]interface{}{
|
||||
db.AssertExists(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
"right": tt.fields.Right,
|
||||
|
@ -407,7 +407,7 @@ func TestNamespaceUser_Delete(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
if !tt.wantErr {
|
||||
db.AssertMissing(t, "users_namespace", map[string]interface{}{
|
||||
db.AssertMissing(t, "users_namespaces", map[string]interface{}{
|
||||
"user_id": tt.fields.UserID,
|
||||
"namespace_id": tt.fields.NamespaceID,
|
||||
})
|
||||
|
|
|
@ -29,7 +29,7 @@ type SavedFilter struct {
|
|||
// The unique numeric id of this saved filter
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"filter"`
|
||||
// The actual filters this filter contains
|
||||
Filters *TaskCollection `xorm:"JSON not null" json:"filters"`
|
||||
Filters *TaskCollection `xorm:"JSON not null" json:"filters" valid:"required"`
|
||||
// The title of the filter.
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250"`
|
||||
// The description of the filter
|
||||
|
@ -39,6 +39,9 @@ type SavedFilter struct {
|
|||
// The user who owns this filter
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
|
||||
// True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite lists.
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
|
||||
// A timestamp when this filter was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this filter was last updated. You cannot change this value.
|
||||
|
@ -90,6 +93,18 @@ func getSavedFiltersForUser(s *xorm.Session, auth web.Auth) (filters []*SavedFil
|
|||
return
|
||||
}
|
||||
|
||||
func (sf *SavedFilter) toList() *List {
|
||||
return &List{
|
||||
ID: getListIDFromSavedFilterID(sf.ID),
|
||||
Title: sf.Title,
|
||||
Description: sf.Description,
|
||||
IsFavorite: sf.IsFavorite,
|
||||
Created: sf.Created,
|
||||
Updated: sf.Updated,
|
||||
Owner: sf.Owner,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new saved filter
|
||||
// @Summary Creates a new saved filter
|
||||
// @Description Creates a new saved filter
|
||||
|
@ -154,12 +169,22 @@ func (sf *SavedFilter) ReadOne(s *xorm.Session, a web.Auth) error {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [post]
|
||||
func (sf *SavedFilter) Update(s *xorm.Session, a web.Auth) error {
|
||||
_, err := s.
|
||||
origFilter, err := getSavedFilterSimpleByID(s, sf.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sf.Filters == nil {
|
||||
sf.Filters = origFilter.Filters
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("id = ?", sf.ID).
|
||||
Cols(
|
||||
"title",
|
||||
"description",
|
||||
"filters",
|
||||
"is_favorite",
|
||||
).
|
||||
Update(sf)
|
||||
return err
|
||||
|
|
|
@ -186,7 +186,7 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
|
|||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("namespace_id").
|
||||
From("list").
|
||||
From("lists").
|
||||
Where(builder.Eq{"id": entityID}),
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
|
@ -203,8 +203,8 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
|
|||
builder.And(
|
||||
builder.Eq{"entity_id": builder.
|
||||
Select("namespace_id").
|
||||
From("list").
|
||||
Join("INNER", "tasks", "list.id = tasks.list_id").
|
||||
From("lists").
|
||||
Join("INNER", "tasks", "lists.id = tasks.list_id").
|
||||
Where(builder.Eq{"tasks.id": entityID}),
|
||||
},
|
||||
builder.Eq{"entity_type": SubscriptionEntityNamespace},
|
||||
|
|
|
@ -64,7 +64,17 @@ func (ta *TaskAttachment) NewAttachment(s *xorm.Session, f io.ReadCloser, realna
|
|||
|
||||
// Add an entry to the db
|
||||
ta.FileID = file.ID
|
||||
ta.CreatedByID = a.GetID()
|
||||
|
||||
ta.CreatedBy, err = GetUserOrLinkShareUser(s, a)
|
||||
if err != nil {
|
||||
// remove the uploaded file if adding it to the db fails
|
||||
if err2 := file.Delete(); err2 != nil {
|
||||
return err2
|
||||
}
|
||||
return err
|
||||
}
|
||||
ta.CreatedByID = ta.CreatedBy.ID
|
||||
|
||||
_, err = s.Insert(ta)
|
||||
if err != nil {
|
||||
// remove the uploaded file if adding it to the db fails
|
||||
|
@ -74,8 +84,6 @@ func (ta *TaskAttachment) NewAttachment(s *xorm.Session, f io.ReadCloser, realna
|
|||
return err
|
||||
}
|
||||
|
||||
ta.CreatedBy, _ = user.GetFromAuth(a) // Ignoring cases where the auth is not a user
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -145,19 +153,19 @@ func (ta *TaskAttachment) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
us := make(map[int64]*user.User)
|
||||
err = s.In("id", userIDs).Find(&us)
|
||||
users, err := getUsersOrLinkSharesFromIDs(s, userIDs)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
for _, r := range attachments {
|
||||
r.CreatedBy = users[r.CreatedByID]
|
||||
|
||||
// If the actual file does not exist, don't try to load it as that would fail with nil panic
|
||||
if _, exists := fs[r.FileID]; !exists {
|
||||
continue
|
||||
}
|
||||
r.File = fs[r.FileID]
|
||||
r.CreatedBy = us[r.CreatedByID]
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = s.
|
||||
|
@ -231,12 +239,9 @@ func getTaskAttachmentsByTaskIDs(s *xorm.Session, taskIDs []int64) (attachments
|
|||
return
|
||||
}
|
||||
|
||||
users := make(map[int64]*user.User)
|
||||
if len(userIDs) > 0 {
|
||||
err = s.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
users, err := getUsersOrLinkSharesFromIDs(s, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Obfuscate all user emails
|
||||
|
|
|
@ -146,8 +146,13 @@ func TestTaskAttachment_ReadAll(t *testing.T) {
|
|||
as, _, _, err := ta.ReadAll(s, &user.User{ID: 1}, "", 0, 50)
|
||||
attachments, _ := as.([]*TaskAttachment)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, attachments, 2)
|
||||
assert.Len(t, attachments, 3)
|
||||
assert.Equal(t, "test", attachments[0].File.Name)
|
||||
for _, a := range attachments {
|
||||
assert.NotNil(t, a.CreatedBy)
|
||||
}
|
||||
assert.Equal(t, int64(-2), attachments[2].CreatedByID)
|
||||
assert.Equal(t, int64(-2), attachments[2].CreatedBy.ID)
|
||||
}
|
||||
|
||||
func TestTaskAttachment_Delete(t *testing.T) {
|
||||
|
|
|
@ -59,6 +59,12 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
linkShareUser2 := &user.User{
|
||||
ID: -2,
|
||||
Name: "Link Share",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
||||
loc := config.GetTimeZone()
|
||||
|
||||
|
@ -124,6 +130,21 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
CreatedBy: user1,
|
||||
Created: testCreatedTime,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
TaskID: 1,
|
||||
FileID: 1,
|
||||
CreatedByID: -2,
|
||||
CreatedBy: linkShareUser2,
|
||||
Created: testCreatedTime,
|
||||
File: &files.File{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
Size: 100,
|
||||
Created: time.Unix(1570998791, 0).In(loc),
|
||||
CreatedByID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
Created: time.Unix(1543626724, 0).In(loc),
|
||||
Updated: time.Unix(1543626724, 0).In(loc),
|
||||
|
@ -985,6 +1006,19 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter by index",
|
||||
fields: fields{
|
||||
FilterBy: []string{"index"},
|
||||
FilterValue: []string{"5"},
|
||||
FilterComparator: []string{"equals"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task5,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -67,24 +67,22 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
tc.AuthorID = a.GetID()
|
||||
tc.Author, err = GetUserOrLinkShareUser(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tc.AuthorID = tc.Author.ID
|
||||
|
||||
_, err = s.Insert(tc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskCommentCreatedEvent{
|
||||
return events.Dispatch(&TaskCommentCreatedEvent{
|
||||
Task: &task,
|
||||
Comment: tc,
|
||||
Doer: doer,
|
||||
Doer: tc.Author,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tc.Author, err = user.GetUserByID(s, a.GetID())
|
||||
return
|
||||
}
|
||||
|
||||
// Delete removes a task comment
|
||||
|
@ -215,14 +213,12 @@ func (tc *TaskComment) ReadAll(s *xorm.Session, auth web.Auth, search string, pa
|
|||
return
|
||||
}
|
||||
|
||||
// Get all authors
|
||||
authors := make(map[int64]*user.User)
|
||||
err = s.
|
||||
Select("users.*").
|
||||
Table("task_comments").
|
||||
Where("task_id = ? AND comment like ?", tc.TaskID, "%"+search+"%").
|
||||
Join("INNER", "users", "users.id = task_comments.author_id").
|
||||
Find(&authors)
|
||||
var authorIDs []int64
|
||||
for _, comment := range comments {
|
||||
authorIDs = append(authorIDs, comment.AuthorID)
|
||||
}
|
||||
|
||||
authors, err := getUsersOrLinkSharesFromIDs(s, authorIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -184,4 +184,18 @@ func TestTaskComment_ReadAll(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.True(t, IsErrGenericForbidden(err))
|
||||
})
|
||||
t.Run("comment from link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{TaskID: 35}
|
||||
u := &user.User{ID: 1}
|
||||
result, _, _, err := tc.ReadAll(s, u, "", 0, -1)
|
||||
comments := result.([]*TaskComment)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, comments, 2)
|
||||
assert.Equal(t, int64(-2), comments[1].AuthorID)
|
||||
assert.NotNil(t, comments[1].Author)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -144,13 +144,17 @@ func (rel *TaskRelation) Create(s *xorm.Session, a web.Auth) error {
|
|||
}
|
||||
}
|
||||
|
||||
rel.CreatedByID = a.GetID()
|
||||
rel.CreatedBy, err = GetUserOrLinkShareUser(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel.CreatedByID = rel.CreatedBy.ID
|
||||
|
||||
// Build up the other relation (see the comment above for explanation)
|
||||
otherRelation := &TaskRelation{
|
||||
TaskID: rel.OtherTaskID,
|
||||
OtherTaskID: rel.TaskID,
|
||||
CreatedByID: a.GetID(),
|
||||
CreatedByID: rel.CreatedByID,
|
||||
}
|
||||
|
||||
switch rel.RelationKind {
|
||||
|
|
|
@ -178,33 +178,34 @@ func (t *Task) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
|
|||
}
|
||||
|
||||
func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err error) {
|
||||
field := "`" + f.field + "`"
|
||||
switch f.comparator {
|
||||
case taskFilterComparatorEquals:
|
||||
cond = &builder.Eq{f.field: f.value}
|
||||
cond = &builder.Eq{field: f.value}
|
||||
case taskFilterComparatorNotEquals:
|
||||
cond = &builder.Neq{f.field: f.value}
|
||||
cond = &builder.Neq{field: f.value}
|
||||
case taskFilterComparatorGreater:
|
||||
cond = &builder.Gt{f.field: f.value}
|
||||
cond = &builder.Gt{field: f.value}
|
||||
case taskFilterComparatorGreateEquals:
|
||||
cond = &builder.Gte{f.field: f.value}
|
||||
cond = &builder.Gte{field: f.value}
|
||||
case taskFilterComparatorLess:
|
||||
cond = &builder.Lt{f.field: f.value}
|
||||
cond = &builder.Lt{field: f.value}
|
||||
case taskFilterComparatorLessEquals:
|
||||
cond = &builder.Lte{f.field: f.value}
|
||||
cond = &builder.Lte{field: f.value}
|
||||
case taskFilterComparatorLike:
|
||||
val, is := f.value.(string)
|
||||
if !is {
|
||||
return nil, ErrInvalidTaskFilterValue{Field: f.field, Value: f.value}
|
||||
return nil, ErrInvalidTaskFilterValue{Field: field, Value: f.value}
|
||||
}
|
||||
cond = &builder.Like{f.field, "%" + val + "%"}
|
||||
cond = &builder.Like{field, "%" + val + "%"}
|
||||
case taskFilterComparatorIn:
|
||||
cond = builder.In(f.field, f.value)
|
||||
cond = builder.In(field, f.value)
|
||||
case taskFilterComparatorInvalid:
|
||||
// Nothing to do
|
||||
}
|
||||
|
||||
if includeNulls {
|
||||
cond = builder.Or(cond, &builder.IsNull{f.field})
|
||||
cond = builder.Or(cond, &builder.IsNull{field})
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -396,7 +397,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
}
|
||||
|
||||
if len(labelFilters) > 0 {
|
||||
filters = append(filters, getFilterCondForSeparateTable("label_task", opts.filterConcat, labelFilters))
|
||||
filters = append(filters, getFilterCondForSeparateTable("label_tasks", opts.filterConcat, labelFilters))
|
||||
}
|
||||
|
||||
if len(namespaceFilters) > 0 {
|
||||
|
@ -412,7 +413,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
"list_id",
|
||||
builder.
|
||||
Select("id").
|
||||
From("list").
|
||||
From("lists").
|
||||
Where(filtercond),
|
||||
)
|
||||
filters = append(filters, cond)
|
||||
|
@ -669,7 +670,7 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
users, err := user.GetUsersByIDs(s, userIDs)
|
||||
users, err := getUsersOrLinkSharesFromIDs(s, userIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -706,34 +707,19 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func checkBucketAndTaskBelongToSameList(s *xorm.Session, fullTask *Task, bucketID int64) (err error) {
|
||||
if bucketID != 0 {
|
||||
b, err := getBucketByID(s, bucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fullTask.ListID != b.ListID {
|
||||
return ErrBucketDoesNotBelongToList{
|
||||
ListID: fullTask.ListID,
|
||||
BucketID: fullTask.BucketID,
|
||||
}
|
||||
func checkBucketAndTaskBelongToSameList(fullTask *Task, bucket *Bucket) (err error) {
|
||||
if fullTask.ListID != bucket.ListID {
|
||||
return ErrBucketDoesNotBelongToList{
|
||||
ListID: fullTask.ListID,
|
||||
BucketID: fullTask.BucketID,
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Checks if adding a new task would exceed the bucket limit
|
||||
func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
|
||||
|
||||
// We need the bucket to check if it has more tasks than the limit allows
|
||||
if bucket == nil {
|
||||
bucket, err = getBucketByID(s, t.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check the limit
|
||||
if bucket.Limit > 0 {
|
||||
taskCount, err := s.
|
||||
Where("bucket_id = ?", bucket.ID).
|
||||
|
@ -748,6 +734,56 @@ func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Contains all the task logic to figure out what bucket to use for this task.
|
||||
func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucketLimit bool) (err error) {
|
||||
// Make sure we have a bucket
|
||||
var bucket *Bucket
|
||||
if task.Done && originalTask != nil && !originalTask.Done {
|
||||
bucket, err := getDoneBucketForList(s, task.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bucket != nil {
|
||||
task.BucketID = bucket.ID
|
||||
}
|
||||
}
|
||||
|
||||
if task.BucketID == 0 || (originalTask != nil && task.ListID != 0 && originalTask.ListID != task.ListID) {
|
||||
bucket, err = getDefaultBucket(s, task.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task.BucketID = bucket.ID
|
||||
}
|
||||
|
||||
if bucket == nil {
|
||||
bucket, err = getBucketByID(s, task.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a bucket set, make sure they belong to the same list as the task
|
||||
err = checkBucketAndTaskBelongToSameList(task, bucket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check the bucket limit
|
||||
// Only check the bucket limit if the task is being moved between buckets, allow reordering the task within a bucket
|
||||
if doCheckBucketLimit {
|
||||
if err := checkBucketLimit(s, task, bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if bucket.IsDoneBucket && originalTask != nil && !originalTask.Done {
|
||||
task.Done = true
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create is the implementation to create a list task
|
||||
// @Summary Create a task
|
||||
// @Description Inserts a task into a list.
|
||||
|
@ -781,44 +817,23 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
return err
|
||||
}
|
||||
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
// A negative user id indicates user share links
|
||||
t.CreatedByID = a.GetID() * -1
|
||||
} else {
|
||||
u, err := user.GetUserByID(s, a.GetID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CreatedByID = u.ID
|
||||
t.CreatedBy = u
|
||||
createdBy, err := GetUserOrLinkShareUser(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CreatedByID = createdBy.ID
|
||||
|
||||
// Generate a uuid if we don't already have one
|
||||
if t.UID == "" {
|
||||
t.UID = utils.MakeRandomString(40)
|
||||
}
|
||||
|
||||
// If there is a bucket set, make sure they belong to the same list as the task
|
||||
err = checkBucketAndTaskBelongToSameList(s, t, t.BucketID)
|
||||
// Get the default bucket and move the task there
|
||||
err = setTaskBucket(s, t, nil, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the default bucket and move the task there
|
||||
var bucket *Bucket
|
||||
if t.BucketID == 0 {
|
||||
bucket, err = getDefaultBucket(s, t.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.BucketID = bucket.ID
|
||||
}
|
||||
|
||||
// Bucket Limit
|
||||
if err := checkBucketLimit(s, t, bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the index for this task
|
||||
latestTask := &Task{}
|
||||
_, err = s.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask)
|
||||
|
@ -835,6 +850,8 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
return err
|
||||
}
|
||||
|
||||
t.CreatedBy = createdBy
|
||||
|
||||
// Update the assignees
|
||||
if updateAssignees {
|
||||
if err := t.updateTaskAssignees(s, t.Assignees, a); err != nil {
|
||||
|
@ -849,10 +866,9 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
|
||||
t.setIdentifier(l)
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskCreatedEvent{
|
||||
Task: t,
|
||||
Doer: doer,
|
||||
Doer: createdBy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -885,6 +901,10 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
if t.ListID == 0 {
|
||||
t.ListID = ot.ListID
|
||||
}
|
||||
|
||||
// Get the reminders
|
||||
reminders, err := getRemindersForTasks(s, []int64{t.ID})
|
||||
if err != nil {
|
||||
|
@ -896,9 +916,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
ot.Reminders[i] = r.Reminder
|
||||
}
|
||||
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(&ot, t)
|
||||
|
||||
// Update the assignees
|
||||
if err := ot.updateTaskAssignees(s, t.Assignees, a); err != nil {
|
||||
return err
|
||||
|
@ -909,12 +926,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// If there is a bucket set, make sure they belong to the same list as the task
|
||||
err = checkBucketAndTaskBelongToSameList(s, &ot, t.BucketID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// All columns to update in a separate variable to be able to add to them
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
|
@ -935,16 +946,14 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
"is_favorite",
|
||||
}
|
||||
|
||||
// Make sure we have a bucket
|
||||
var bucket *Bucket
|
||||
if t.BucketID == 0 || (t.ListID != 0 && ot.ListID != t.ListID) {
|
||||
bucket, err = getDefaultBucket(s, t.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.BucketID = bucket.ID
|
||||
err = setTaskBucket(s, t, &ot, t.BucketID != ot.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(&ot, t)
|
||||
|
||||
// If the task is being moved between lists, make sure to move the bucket + index as well
|
||||
if t.ListID != 0 && ot.ListID != t.ListID {
|
||||
latestTask := &Task{}
|
||||
|
@ -957,14 +966,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
colsToUpdate = append(colsToUpdate, "index")
|
||||
}
|
||||
|
||||
// Check the bucket limit
|
||||
// Only check the bucket limit if the task is being moved between buckets, allow reordering the task within a bucket
|
||||
if t.BucketID != ot.BucketID {
|
||||
if err := checkBucketLimit(s, t, bucket); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update the labels
|
||||
//
|
||||
// Maybe FIXME:
|
||||
|
|
|
@ -202,6 +202,106 @@ func TestTask_Update(t *testing.T) {
|
|||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("bucket on other list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
BucketID: 4, // Bucket 4 belongs to list 2
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrBucketDoesNotBelongToList(err))
|
||||
})
|
||||
t.Run("moving a task to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test",
|
||||
ListID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, task.Done)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"done": true,
|
||||
"title": "test",
|
||||
"list_id": 1,
|
||||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("default bucket when moving a task between lists", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
ListID: 2,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(4), task.BucketID) // bucket 4 is the default bucket on list 2
|
||||
assert.True(t, task.Done) // bucket 4 is the done bucket, so the task should be marked as done as well
|
||||
})
|
||||
t.Run("marking a task as done should move it to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Done: true,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, task.Done)
|
||||
assert.Equal(t, int64(3), task.BucketID)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"done": true,
|
||||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("move task to another list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
ListID: 2,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 1,
|
||||
"list_id": 2,
|
||||
"bucket_id": 4,
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTask_Delete(t *testing.T) {
|
||||
|
@ -479,4 +579,17 @@ func TestTask_ReadOne(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.NotNil(t, task.Subscription)
|
||||
})
|
||||
t.Run("created by link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{ID: 37}
|
||||
err := task.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "task #37", task.Title)
|
||||
assert.Equal(t, int64(-2), task.CreatedByID)
|
||||
assert.NotNil(t, task.CreatedBy)
|
||||
assert.Equal(t, int64(-2), task.CreatedBy.ID)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -39,10 +39,10 @@ func SetupTests() {
|
|||
|
||||
err = db.InitTestFixtures(
|
||||
"files",
|
||||
"label_task",
|
||||
"label_tasks",
|
||||
"labels",
|
||||
"link_sharing",
|
||||
"list",
|
||||
"link_shares",
|
||||
"lists",
|
||||
"namespaces",
|
||||
"task_assignees",
|
||||
"task_attachments",
|
||||
|
@ -50,13 +50,13 @@ func SetupTests() {
|
|||
"task_relations",
|
||||
"task_reminders",
|
||||
"tasks",
|
||||
"team_list",
|
||||
"team_lists",
|
||||
"team_members",
|
||||
"team_namespaces",
|
||||
"teams",
|
||||
"users",
|
||||
"users_list",
|
||||
"users_namespace",
|
||||
"users_lists",
|
||||
"users_namespaces",
|
||||
"buckets",
|
||||
"saved_filters",
|
||||
"subscriptions",
|
||||
|
|
|
@ -44,16 +44,16 @@ func ListUsersFromList(s *xorm.Session, l *List, search string) (users []*user.U
|
|||
n.owner_id as nOwner,
|
||||
tm.user_id as tnUID,
|
||||
tm2.user_id as tlUID`).
|
||||
Table("list").
|
||||
Table("lists").
|
||||
Alias("l").
|
||||
// User stuff
|
||||
Join("LEFT", []string{"users_namespace", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_list", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"users_namespaces", "un"}, "un.namespace_id = l.namespace_id").
|
||||
Join("LEFT", []string{"users_lists", "ul"}, "ul.list_id = l.id").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "n.id = l.namespace_id").
|
||||
// Team stuff
|
||||
Join("LEFT", []string{"team_namespaces", "tn"}, " l.namespace_id = tn.namespace_id").
|
||||
Join("LEFT", []string{"team_members", "tm"}, "tm.team_id = tn.team_id").
|
||||
Join("LEFT", []string{"team_list", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_lists", "tl"}, "l.id = tl.list_id").
|
||||
Join("LEFT", []string{"team_members", "tm2"}, "tm2.team_id = tl.team_id").
|
||||
// The actual condition
|
||||
Where(
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// GetUserOrLinkShareUser returns either a user or a link share disguised as a user.
|
||||
func GetUserOrLinkShareUser(s *xorm.Session, a web.Auth) (uu *user.User, err error) {
|
||||
if u, is := a.(*user.User); is {
|
||||
uu, err = user.GetUserByID(s, u.ID)
|
||||
return
|
||||
}
|
||||
|
||||
if ls, is := a.(*LinkSharing); is {
|
||||
l, err := GetLinkShareByID(s, ls.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return l.toUser(), nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Returns all users or pseudo link shares from a slice of ids. ids < 0 are considered to be a link share in that case.
|
||||
func getUsersOrLinkSharesFromIDs(s *xorm.Session, ids []int64) (users map[int64]*user.User, err error) {
|
||||
users = make(map[int64]*user.User)
|
||||
var userIDs []int64
|
||||
var linkShareIDs []int64
|
||||
for _, id := range ids {
|
||||
if id < 0 {
|
||||
linkShareIDs = append(linkShareIDs, id*-1)
|
||||
continue
|
||||
}
|
||||
|
||||
userIDs = append(userIDs, id)
|
||||
}
|
||||
|
||||
if len(userIDs) > 0 {
|
||||
users, err = user.GetUsersByIDs(s, userIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(linkShareIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
shares, err := GetLinkSharesByIDs(s, linkShareIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, share := range shares {
|
||||
users[share.ID*-1] = share.toUser()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
|
@ -93,6 +93,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
DiscoverableByEmail: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -129,6 +130,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
testuser11 := &user.User{
|
||||
ID: 11,
|
||||
Username: "user11",
|
||||
Name: "Some one else",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
|
@ -139,10 +141,12 @@ func TestListUsersFromList(t *testing.T) {
|
|||
testuser12 := &user.User{
|
||||
ID: 12,
|
||||
Username: "user12",
|
||||
Name: "Name with spaces",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
DiscoverableByName: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
|
|
@ -199,6 +199,33 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
func checkListBackgroundRights(s *xorm.Session, c echo.Context) (list *models.List, auth web.Auth, err error) {
|
||||
auth, err = auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error())
|
||||
}
|
||||
|
||||
listID, err := strconv.ParseInt(c.Param("list"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid list ID: "+err.Error())
|
||||
}
|
||||
|
||||
// Check if a background for this list exists + Rights
|
||||
list = &models.List{ID: listID}
|
||||
can, _, err := list.CanRead(s, auth)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return nil, auth, handler.HandleHTTPError(err, c)
|
||||
}
|
||||
if !can {
|
||||
_ = s.Rollback()
|
||||
log.Infof("Tried to get list background of list %d while not having the rights for it (User: %v)", listID, auth)
|
||||
return nil, auth, echo.NewHTTPError(http.StatusForbidden)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetListBackground serves a previously set background from a list
|
||||
// It has no knowledge of the provider that was responsible for setting the background.
|
||||
// @Summary Get the list background
|
||||
|
@ -214,31 +241,14 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
|
|||
// @Router /lists/{id}/background [get]
|
||||
func GetListBackground(c echo.Context) error {
|
||||
|
||||
auth, err := auth2.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error())
|
||||
}
|
||||
|
||||
listID, err := strconv.ParseInt(c.Param("list"), 10, 64)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid list ID: "+err.Error())
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
// Check if a background for this list exists + Rights
|
||||
list := &models.List{ID: listID}
|
||||
can, _, err := list.CanRead(s, auth)
|
||||
list, _, err := checkListBackgroundRights(s, c)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
if !can {
|
||||
_ = s.Rollback()
|
||||
log.Infof("Tried to get list background of list %d while not having the rights for it (User: %v)", listID, auth)
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
return err
|
||||
}
|
||||
|
||||
if list.BackgroundFileID == 0 {
|
||||
_ = s.Rollback()
|
||||
return echo.NotFoundHandler(c)
|
||||
|
@ -266,3 +276,34 @@ func GetListBackground(c echo.Context) error {
|
|||
// Serve the file
|
||||
return c.Stream(http.StatusOK, "image/jpg", bgFile.File)
|
||||
}
|
||||
|
||||
// RemoveListBackground removes a list background, no matter the background provider
|
||||
// @Summary Remove a list background
|
||||
// @Description Removes a previously set list background, regardless of the list provider used to set the background. It does not throw an error if the list does not have a background.
|
||||
// @tags list
|
||||
// @Produce json
|
||||
// @Param id path int true "List ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} models.List "The list"
|
||||
// @Failure 403 {object} models.Message "No access to this list."
|
||||
// @Failure 404 {object} models.Message "The list does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/background [delete]
|
||||
func RemoveListBackground(c echo.Context) error {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
list, auth, err := checkListBackgroundRights(s, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list.BackgroundFileID = 0
|
||||
list.BackgroundInformation = nil
|
||||
err = list.Update(s, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
"title": testStructure[0].Namespace.Title,
|
||||
"description": testStructure[0].Namespace.Description,
|
||||
}, false)
|
||||
db.AssertExists(t, "list", map[string]interface{}{
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"title": testStructure[0].Lists[0].Title,
|
||||
"description": testStructure[0].Lists[0].Description,
|
||||
}, false)
|
||||
|
|
|
@ -62,14 +62,13 @@ func GetAvatar(c echo.Context) error {
|
|||
|
||||
// Get the user
|
||||
u, err := user.GetUserWithEmail(s, &user.User{Username: username})
|
||||
if err != nil {
|
||||
if err != nil && !user.IsErrUserDoesNotExist(err) {
|
||||
log.Errorf("Error getting user for avatar: %v", err)
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Initialize the avatar provider
|
||||
// For now, we only have one avatar provider, in the future there could be multiple which
|
||||
// could be changed based on user settings etc.
|
||||
found := !(err != nil && user.IsErrUserDoesNotExist(err))
|
||||
|
||||
var avatarProvider avatar.Provider
|
||||
switch u.AvatarProvider {
|
||||
case "gravatar":
|
||||
|
@ -82,6 +81,10 @@ func GetAvatar(c echo.Context) error {
|
|||
avatarProvider = &empty.Provider{}
|
||||
}
|
||||
|
||||
if !found {
|
||||
avatarProvider = &empty.Provider{}
|
||||
}
|
||||
|
||||
size := c.QueryParam("size")
|
||||
var sizeInt int64 = 250 // Default size of 250
|
||||
if size != "" {
|
||||
|
|
|
@ -31,11 +31,11 @@ import (
|
|||
|
||||
// UserList gets all information about a user
|
||||
// @Summary Get users
|
||||
// @Description Lists all users (without emailadresses). Also possible to search for a specific user.
|
||||
// @Description Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param s query string false "Search for a user by its name."
|
||||
// @Param s query string false "The search criteria."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} user.User "All (found) users."
|
||||
// @Failure 400 {object} web.HTTPError "Something's invalid."
|
||||
|
|
|
@ -38,7 +38,11 @@ type UserSettings struct {
|
|||
// The new name of the current user.
|
||||
Name string `json:"name"`
|
||||
// If enabled, sends email reminders of tasks to the user.
|
||||
EmailRemindersEnabled bool `xorm:"bool default false" json:"email_reminders_enabled"`
|
||||
EmailRemindersEnabled bool `json:"email_reminders_enabled"`
|
||||
// If true, this user can be found by their name or parts of it when searching for it.
|
||||
DiscoverableByName bool `json:"discoverable_by_name"`
|
||||
// If true, the user can be found when searching for their exact email.
|
||||
DiscoverableByEmail bool `json:"discoverable_by_email"`
|
||||
}
|
||||
|
||||
// GetUserAvatarProvider returns the currently set user avatar
|
||||
|
@ -161,6 +165,8 @@ func UpdateGeneralUserSettings(c echo.Context) error {
|
|||
|
||||
user.Name = us.Name
|
||||
user.EmailRemindersEnabled = us.EmailRemindersEnabled
|
||||
user.DiscoverableByEmail = us.DiscoverableByEmail
|
||||
user.DiscoverableByName = us.DiscoverableByName
|
||||
|
||||
_, err = user2.UpdateUser(s, user)
|
||||
if err != nil {
|
||||
|
|
|
@ -19,13 +19,22 @@ package v1
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type userWithSettings struct {
|
||||
user.User
|
||||
Settings *UserSettings `json:"settings"`
|
||||
}
|
||||
|
||||
// UserShow gets all informations about the current user
|
||||
// @Summary Get user information
|
||||
// @Description Returns the current user object.
|
||||
|
@ -38,7 +47,7 @@ import (
|
|||
// @Failure 500 {object} models.Message "Internal server error."
|
||||
// @Router /user [get]
|
||||
func UserShow(c echo.Context) error {
|
||||
userInfos, err := user2.GetCurrentUser(c)
|
||||
a, err := auth.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Error getting current user.")
|
||||
}
|
||||
|
@ -46,16 +55,20 @@ func UserShow(c echo.Context) error {
|
|||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
user, err := user2.GetUserByID(s, userInfos.ID)
|
||||
u, err := models.GetUserOrLinkShareUser(s, a)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
us := &userWithSettings{
|
||||
User: *u,
|
||||
Settings: &UserSettings{
|
||||
Name: u.Name,
|
||||
EmailRemindersEnabled: u.EmailRemindersEnabled,
|
||||
DiscoverableByName: u.DiscoverableByName,
|
||||
DiscoverableByEmail: u.DiscoverableByEmail,
|
||||
},
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, user)
|
||||
return c.JSON(http.StatusOK, us)
|
||||
}
|
||||
|
|
|
@ -576,6 +576,7 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
// List Backgrounds
|
||||
if config.BackgroundsEnabled.GetBool() {
|
||||
a.GET("/lists/:list/background", backgroundHandler.GetListBackground)
|
||||
a.DELETE("/lists/:list/background", backgroundHandler.RemoveListBackground)
|
||||
if config.BackgroundsUploadEnabled.GetBool() {
|
||||
uploadBackgroundProvider := &backgroundHandler.BackgroundProvider{
|
||||
Provider: func() background.Provider {
|
||||
|
|
|
@ -1063,6 +1063,56 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Removes a previously set list background, regardless of the list provider used to set the background. It does not throw an error if the list does not have a background.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"list"
|
||||
],
|
||||
"summary": "Remove a list background",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "List ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.List"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "No access to this list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The list does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/lists/{id}/backgrounds/unsplash": {
|
||||
|
@ -6926,7 +6976,7 @@ var doc = `{
|
|||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Lists all users (without emailadresses). Also possible to search for a specific user.",
|
||||
"description": "Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -6940,7 +6990,7 @@ var doc = `{
|
|||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search for a user by its name.",
|
||||
"description": "The search criteria.",
|
||||
"name": "s",
|
||||
"in": "query"
|
||||
}
|
||||
|
@ -7139,6 +7189,10 @@ var doc = `{
|
|||
"description": "The unique, numeric id of this bucket.",
|
||||
"type": "integer"
|
||||
},
|
||||
"is_done_bucket": {
|
||||
"description": "If this bucket is the \"done bucket\". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"limit": {
|
||||
"description": "How many tasks can be at the same time on this board max",
|
||||
"type": "integer"
|
||||
|
@ -7435,6 +7489,10 @@ var doc = `{
|
|||
"description": "The ID of the shared thing",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.",
|
||||
"type": "string"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
|
||||
"type": "integer",
|
||||
|
@ -7716,6 +7774,10 @@ var doc = `{
|
|||
"description": "The unique numeric id of this saved filter",
|
||||
"type": "integer"
|
||||
},
|
||||
"is_favorite": {
|
||||
"description": "True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite lists.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The user who owns this filter",
|
||||
"$ref": "#/definitions/user.User"
|
||||
|
@ -8472,6 +8534,14 @@ var doc = `{
|
|||
"v1.UserSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discoverable_by_email": {
|
||||
"description": "If true, the user can be found when searching for their exact email.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"discoverable_by_name": {
|
||||
"description": "If true, this user can be found by their name or parts of it when searching for it.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"email_reminders_enabled": {
|
||||
"description": "If enabled, sends email reminders of tasks to the user.",
|
||||
"type": "boolean"
|
||||
|
|
|
@ -1046,6 +1046,56 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Removes a previously set list background, regardless of the list provider used to set the background. It does not throw an error if the list does not have a background.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"list"
|
||||
],
|
||||
"summary": "Remove a list background",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "List ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.List"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "No access to this list.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The list does not exist.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/lists/{id}/backgrounds/unsplash": {
|
||||
|
@ -6909,7 +6959,7 @@
|
|||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Lists all users (without emailadresses). Also possible to search for a specific user.",
|
||||
"description": "Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -6923,7 +6973,7 @@
|
|||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search for a user by its name.",
|
||||
"description": "The search criteria.",
|
||||
"name": "s",
|
||||
"in": "query"
|
||||
}
|
||||
|
@ -7122,6 +7172,10 @@
|
|||
"description": "The unique, numeric id of this bucket.",
|
||||
"type": "integer"
|
||||
},
|
||||
"is_done_bucket": {
|
||||
"description": "If this bucket is the \"done bucket\". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"limit": {
|
||||
"description": "How many tasks can be at the same time on this board max",
|
||||
"type": "integer"
|
||||
|
@ -7418,6 +7472,10 @@
|
|||
"description": "The ID of the shared thing",
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.",
|
||||
"type": "string"
|
||||
},
|
||||
"right": {
|
||||
"description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
|
||||
"type": "integer",
|
||||
|
@ -7699,6 +7757,10 @@
|
|||
"description": "The unique numeric id of this saved filter",
|
||||
"type": "integer"
|
||||
},
|
||||
"is_favorite": {
|
||||
"description": "True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite lists.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"owner": {
|
||||
"description": "The user who owns this filter",
|
||||
"$ref": "#/definitions/user.User"
|
||||
|
@ -8455,6 +8517,14 @@
|
|||
"v1.UserSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"discoverable_by_email": {
|
||||
"description": "If true, the user can be found when searching for their exact email.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"discoverable_by_name": {
|
||||
"description": "If true, this user can be found by their name or parts of it when searching for it.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"email_reminders_enabled": {
|
||||
"description": "If enabled, sends email reminders of tasks to the user.",
|
||||
"type": "boolean"
|
||||
|
|
|
@ -81,6 +81,9 @@ definitions:
|
|||
id:
|
||||
description: The unique, numeric id of this bucket.
|
||||
type: integer
|
||||
is_done_bucket:
|
||||
description: If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.
|
||||
type: boolean
|
||||
limit:
|
||||
description: How many tasks can be at the same time on this board max
|
||||
type: integer
|
||||
|
@ -307,6 +310,9 @@ definitions:
|
|||
id:
|
||||
description: The ID of the shared thing
|
||||
type: integer
|
||||
name:
|
||||
description: The name of this link share. All actions someone takes while being authenticated with that link will appear with that name.
|
||||
type: string
|
||||
right:
|
||||
default: 0
|
||||
description: The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
|
||||
|
@ -521,6 +527,9 @@ definitions:
|
|||
id:
|
||||
description: The unique numeric id of this saved filter
|
||||
type: integer
|
||||
is_favorite:
|
||||
description: True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite lists.
|
||||
type: boolean
|
||||
owner:
|
||||
$ref: '#/definitions/user.User'
|
||||
description: The user who owns this filter
|
||||
|
@ -1070,6 +1079,12 @@ definitions:
|
|||
type: object
|
||||
v1.UserSettings:
|
||||
properties:
|
||||
discoverable_by_email:
|
||||
description: If true, the user can be found when searching for their exact email.
|
||||
type: boolean
|
||||
discoverable_by_name:
|
||||
description: If true, this user can be found by their name or parts of it when searching for it.
|
||||
type: boolean
|
||||
email_reminders_enabled:
|
||||
description: If enabled, sends email reminders of tasks to the user.
|
||||
type: boolean
|
||||
|
@ -1835,6 +1850,38 @@ paths:
|
|||
tags:
|
||||
- task
|
||||
/lists/{id}/background:
|
||||
delete:
|
||||
description: Removes a previously set list background, regardless of the list provider used to set the background. It does not throw an error if the list does not have a background.
|
||||
parameters:
|
||||
- description: List ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: integer
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The list
|
||||
schema:
|
||||
$ref: '#/definitions/models.List'
|
||||
"403":
|
||||
description: No access to this list.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"404":
|
||||
description: The list does not exist.
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Remove a list background
|
||||
tags:
|
||||
- list
|
||||
get:
|
||||
description: Get the list background of a specific list. **Returns json on error.**
|
||||
parameters:
|
||||
|
@ -5621,9 +5668,9 @@ paths:
|
|||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Lists all users (without emailadresses). Also possible to search for a specific user.
|
||||
description: Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings.
|
||||
parameters:
|
||||
- description: Search for a user by its name.
|
||||
- description: The search criteria.
|
||||
in: query
|
||||
name: s
|
||||
type: string
|
||||
|
|
|
@ -61,7 +61,7 @@ type User struct {
|
|||
EmailConfirmToken string `xorm:"varchar(450) null" json:"-"`
|
||||
|
||||
AvatarProvider string `xorm:"varchar(255) null" json:"-"`
|
||||
AvatarFileID int64 `xorn:"null" json:"-"`
|
||||
AvatarFileID int64 `xorm:"null" json:"-"`
|
||||
|
||||
// Issuer and Subject contain the issuer and subject from the source the user authenticated with.
|
||||
Issuer string `xorm:"text null" json:"-"`
|
||||
|
@ -70,6 +70,9 @@ type User struct {
|
|||
// If enabled, sends email reminders of tasks to the user.
|
||||
EmailRemindersEnabled bool `xorm:"bool default true" json:"-"`
|
||||
|
||||
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
|
||||
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
|
||||
|
||||
// A timestamp when this task was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this task was last updated. You cannot change this value.
|
||||
|
@ -366,6 +369,8 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) {
|
|||
"is_active",
|
||||
"name",
|
||||
"email_reminders_enabled",
|
||||
"discoverable_by_name",
|
||||
"discoverable_by_email",
|
||||
).
|
||||
Update(user)
|
||||
if err != nil {
|
||||
|
|
|
@ -373,10 +373,63 @@ func TestListUsers(t *testing.T) {
|
|||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
all, err := ListUsers(s, "")
|
||||
all, err := ListAllUsers(s)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 14)
|
||||
})
|
||||
t.Run("no search term", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
all, err := ListUsers(s, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 0)
|
||||
})
|
||||
t.Run("not discoverable by email", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
all, err := ListUsers(s, "user1@example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 0)
|
||||
db.AssertExists(t, "users", map[string]interface{}{
|
||||
"email": "user1@example.com",
|
||||
}, false)
|
||||
})
|
||||
t.Run("not discoverable by name", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
all, err := ListUsers(s, "one else")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 0)
|
||||
db.AssertExists(t, "users", map[string]interface{}{
|
||||
"name": "Some one else",
|
||||
}, false)
|
||||
})
|
||||
t.Run("discoverable by email", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
all, err := ListUsers(s, "user7@example.com")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 1)
|
||||
assert.Equal(t, int64(7), all[0].ID)
|
||||
})
|
||||
t.Run("discoverable by partial name", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
all, err := ListUsers(s, "with space")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, all, 1)
|
||||
assert.Equal(t, int64(12), all[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserPasswordReset(t *testing.T) {
|
||||
|
|
|
@ -17,42 +17,40 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
)
|
||||
|
||||
// ListUsers returns a list with all users, filtered by an optional searchstring
|
||||
func ListUsers(s *xorm.Session, searchterm string) (users []*User, err error) {
|
||||
func ListUsers(s *xorm.Session, search string) (users []*User, err error) {
|
||||
|
||||
vals := strings.Split(searchterm, ",")
|
||||
ids := []int64{}
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
log.Debugf("User search string part '%s' is not a number: %s", val, err)
|
||||
continue
|
||||
}
|
||||
ids = append(ids, v)
|
||||
}
|
||||
// Prevent searching for placeholders
|
||||
search = strings.ReplaceAll(search, "%", "")
|
||||
|
||||
if len(ids) > 0 {
|
||||
err = s.
|
||||
In("id", ids).
|
||||
Find(&users)
|
||||
return
|
||||
}
|
||||
|
||||
if searchterm == "" {
|
||||
err = s.Find(&users)
|
||||
if search == "" || strings.ReplaceAll(search, " ", "") == "" {
|
||||
return
|
||||
}
|
||||
|
||||
err = s.
|
||||
Where("username LIKE ?", "%"+searchterm+"%").
|
||||
Where(builder.Or(
|
||||
builder.Like{"username", "%" + search + "%"},
|
||||
builder.And(
|
||||
builder.Eq{"email": search},
|
||||
builder.Eq{"discoverable_by_email": true},
|
||||
),
|
||||
builder.And(
|
||||
builder.Like{"name", "%" + search + "%"},
|
||||
builder.Eq{"discoverable_by_name": true},
|
||||
),
|
||||
)).
|
||||
Find(&users)
|
||||
return
|
||||
}
|
||||
|
||||
// ListAllUsers returns all users
|
||||
func ListAllUsers(s *xorm.Session) (users []*User, err error) {
|
||||
err = s.Find(&users)
|
||||
return
|
||||
}
|
||||
|
|
9
tools.go
9
tools.go
|
@ -21,18 +21,9 @@ package tools
|
|||
// This file is needed for go mod to recognize the tools we use.
|
||||
|
||||
import (
|
||||
_ "github.com/client9/misspell/cmd/misspell"
|
||||
_ "github.com/cweill/gotests"
|
||||
_ "github.com/fzipp/gocyclo"
|
||||
_ "github.com/gordonklaus/ineffassign"
|
||||
_ "github.com/swaggo/swag/cmd/swag"
|
||||
_ "golang.org/x/lint/golint"
|
||||
_ "src.techknowlogick.com/xgo"
|
||||
|
||||
_ "github.com/jgautheron/goconst/cmd/goconst"
|
||||
_ "honnef.co/go/tools/cmd/staticcheck"
|
||||
|
||||
_ "github.com/shurcooL/vfsgen"
|
||||
|
||||
_ "github.com/magefile/mage"
|
||||
)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
[Unit]
|
||||
Description=Vikunja
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
# Depending on how you configured Vikunja, you may want to uncomment these:
|
||||
#Requires=mysql.service
|
||||
#Requires=mariadb.service
|
||||
#Requires=postgresql.service
|
||||
#Requires=redis.service
|
||||
|
||||
[Service]
|
||||
RestartSec=2s
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/vikunja
|
||||
ExecStart=/usr/local/bin/vikunja
|
||||
Restart=always
|
||||
# If you want to bind Vikunja to a port below 1024 uncomment
|
||||
# the two values below
|
||||
###
|
||||
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||
#AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
Loading…
Reference in New Issue