Merge branch 'main' into renovate/github.com-go-redis-redis-v8-8.x

# Conflicts:
#	go.mod
This commit is contained in:
kolaente 2021-04-09 13:36:42 +02:00
commit 7c6c767e70
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
81 changed files with 1693 additions and 474 deletions

View File

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

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ files/
vikunja-dump*
vendor/
os-packages/
mage_output_file.go

View File

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

View File

@ -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
View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type 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
},
})
}

View File

@ -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
},
})
}

View File

@ -0,0 +1,43 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type 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
},
})
}

View File

@ -0,0 +1,43 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type 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
},
})
}

View File

@ -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
},
})
}

View File

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

View File

@ -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
// =============

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
})
}

View File

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

View File

@ -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,
})

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
})

View File

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

View File

@ -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},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

78
pkg/models/users.go Normal file
View File

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

View File

@ -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,
}

View File

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

View File

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

View File

@ -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 != "" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

24
vikunja.service Normal file
View File

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