Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ddd5a31a | ||
|
|
387aa2db93 | ||
|
|
4f62e978ef | ||
|
|
a5c241758a | ||
|
|
abf38defa2 | ||
|
|
29f8522de9 | ||
|
|
9f14466dfa | ||
|
|
4a82f3be3c | ||
|
|
a39b3aeb06 | ||
|
|
b4c215f4dd | ||
|
|
f0a8516926 | ||
|
|
077baba2ea | ||
|
|
066c26f83e | ||
|
|
eda5135b3c | ||
|
|
906c9fc06b | ||
|
|
e37dbb1648 | ||
|
|
cd13f86e0d | ||
|
|
7ad754dd3b | ||
|
|
a62b57ac62 | ||
|
|
534d04a1db | ||
|
|
e8c85562b1 | ||
|
|
1afc72e190 | ||
|
|
53197b85e3 | ||
|
|
d47535b831 | ||
|
|
c9ce9c2382 | ||
|
|
168287923f | ||
|
|
ca6d1946da | ||
|
|
8b745ad599 | ||
|
|
1efa1696bf | ||
|
|
6908167fb2 | ||
|
|
26b280019b | ||
|
|
d19529e84f | ||
|
|
0f7f595cc3 | ||
|
|
c6769d407e | ||
|
|
8df94e8800 | ||
|
|
5af1147e0d | ||
|
|
8bf224d392 | ||
|
|
0a0374e268 | ||
|
|
da9d25cf72 | ||
|
|
eb33655c1c | ||
|
|
20a5994b17 | ||
|
|
5a0cee2fc5 | ||
|
|
fceb5dae0f | ||
|
|
659803fadf | ||
|
|
4fbe2b313b | ||
|
|
a6a47a27cb | ||
|
|
247f678491 | ||
|
|
7d796cea0a | ||
|
|
984bcb90e1 | ||
|
|
6bbcd1399f | ||
|
|
58da38adb6 | ||
|
|
4a05d1a497 | ||
|
|
d6643f5af3 | ||
|
|
aa25ccdc91 | ||
|
|
cb96590611 | ||
|
|
5d242f7e54 | ||
|
|
02352841dc | ||
|
|
d682a22cd5 | ||
|
|
fdbe110945 | ||
|
|
7b46446e03 | ||
|
|
3b1887e438 | ||
|
|
32ff4b2cbd | ||
|
|
6ddadba573 | ||
|
|
afdceb0aff | ||
|
|
437960b146 | ||
|
|
75baba32a6 | ||
|
|
9432b437fe | ||
|
|
ef8e97f95e | ||
|
|
522bf7d2fc | ||
|
|
88dd544fc9 | ||
|
|
f660badc3d | ||
|
|
491a142378 | ||
|
|
46b261c9fe | ||
|
|
40411e4100 | ||
|
|
f2b4e9260b | ||
|
|
682123a9c9 | ||
|
|
0cd9cd324e | ||
|
|
100897cc9d | ||
|
|
c59e006453 | ||
|
|
6a87d919fa | ||
|
|
d52816c7d7 | ||
|
|
87d0134bb2 | ||
|
|
d19fc80b8b | ||
|
|
fecce19f06 | ||
|
|
1971df7b84 | ||
|
|
31a1452839 | ||
|
|
530bb0a63c | ||
|
|
7bf7a13bb9 | ||
|
|
10e6843b11 | ||
|
|
78f43829cf | ||
|
|
acd31c4ff7 | ||
|
|
1c02114cf5 | ||
|
|
82f4a5ad50 | ||
|
|
04614614fe | ||
|
|
608bde9806 | ||
|
|
568cc16797 | ||
|
|
4b5e65d4c2 | ||
|
|
3329d83363 | ||
|
|
c4e5e722e4 | ||
|
|
508a3157e2 | ||
|
|
321a8f7e2b | ||
|
|
dd7dcdd0cc | ||
|
|
cb6368036c | ||
|
|
36dfcb8ddb | ||
|
|
7f8c85118e | ||
|
|
f3476bec6c | ||
|
|
392bdd1b94 | ||
|
|
80634d43c1 | ||
|
|
4fa45bf9dc | ||
|
|
ef1d1e2b20 | ||
|
|
ca3580766e | ||
|
|
c6429c8b13 | ||
|
|
304481cf28 | ||
|
|
897a6e5d5c | ||
|
|
194b88e2eb | ||
|
|
c5327845ee | ||
|
|
ea1d06bda6 | ||
|
|
0104aa504b | ||
|
|
6a97a214a3 | ||
|
|
a79b1de2d0 | ||
|
|
e9ce930230 | ||
|
|
6cb48e430e | ||
|
|
a2c8426d02 | ||
|
|
3be10ca4a2 | ||
|
|
ec297009d3 | ||
|
|
dbc30284f3 | ||
|
|
879324dcd0 | ||
|
|
3be6e93a05 | ||
|
|
f93317bf5d | ||
|
|
1cfdb085e5 | ||
|
|
51cf8beaed | ||
|
|
b8c3b570a4 | ||
|
|
8ae062a095 | ||
|
|
941d1e06c5 | ||
|
|
1f2eb57602 | ||
|
|
51911a8868 | ||
|
|
47aae115df | ||
|
|
fbc4b91e0f | ||
|
|
8c67be558f | ||
|
|
e27cd9b336 | ||
|
|
23b01a1ff6 | ||
|
|
f47faf577a | ||
|
|
312525ebef | ||
|
|
a17d2f4288 | ||
|
|
7e0aa20658 | ||
|
|
c5a55e39bf | ||
|
|
4d4ffe8b34 | ||
|
|
3d9fcb9ffb | ||
|
|
a6e214b654 | ||
|
|
c47e07f9b0 | ||
|
|
3a4a04ee8e | ||
|
|
96b5e93379 | ||
|
|
87c2e442f2 | ||
|
|
33e27c66a0 | ||
|
|
986129a784 | ||
|
|
3d7605591e | ||
|
|
811514855b | ||
|
|
a9e6776abf | ||
|
|
15828df041 | ||
|
|
f9b48ec091 | ||
|
|
3b0b4a8460 | ||
|
|
2ef5e54588 | ||
|
|
65bca226e0 | ||
|
|
63a9148132 | ||
|
|
b880d0e300 | ||
|
|
2c46fc25d4 | ||
|
|
641a9da93d | ||
|
|
4d4dca12ef | ||
|
|
622f2f0562 | ||
|
|
c495096444 | ||
|
|
649d1e3e6f | ||
|
|
c83cb8480d | ||
|
|
556abcd9d2 | ||
|
|
b10dbce1a1 | ||
|
|
7b77974b03 | ||
|
|
05358350af | ||
|
|
9fc08a0790 | ||
|
|
f6b897e8e7 | ||
|
|
8de78c48f8 | ||
|
|
a13126d1dd | ||
|
|
b96e681270 | ||
|
|
f5fd849a0b | ||
|
|
144e115394 | ||
|
|
815fc10135 | ||
|
|
955a1771ae | ||
|
|
aca930655b | ||
|
|
2ba78d240f | ||
|
|
cad18945bb | ||
|
|
9fc0fc184d | ||
|
|
1577c8d3f3 | ||
|
|
2eb4d07aa9 | ||
|
|
4789a69455 | ||
|
|
35f01a4549 | ||
|
|
dca51c762b | ||
|
|
6515dd6908 | ||
|
|
0ea4de3f56 | ||
|
|
c2104a3374 | ||
|
|
aaceb4e968 | ||
|
|
4ec4c0a65d | ||
|
|
c68bd235e8 | ||
|
|
df2e36c2a3 | ||
|
|
f5a33478f2 | ||
|
|
0d044997df | ||
|
|
5e40f4ec89 | ||
|
|
5871d32c2d | ||
|
|
3af9855148 | ||
|
|
e5394d6d4b | ||
|
|
b8769c746c | ||
|
|
b331fdd29a | ||
|
|
2fc690a783 | ||
|
|
bcb286a7f0 | ||
|
|
008908eb49 | ||
|
|
12e0e12bae | ||
|
|
d43762e9d9 | ||
|
|
631a265d2d | ||
|
|
e113fe34d0 | ||
|
|
0eb47096db | ||
|
|
0e1904d50b | ||
|
|
b4b25499f2 | ||
|
|
b735ffc4b3 | ||
|
|
95105aaa35 | ||
|
|
66331b1002 | ||
|
|
ed6a27da6a | ||
|
|
b5ee39b887 | ||
|
|
0d8451ab6e | ||
|
|
81f09f7dc0 | ||
|
|
5a40100ac5 | ||
|
|
0694314e52 | ||
|
|
a547a9eb25 | ||
|
|
0fcd03f561 | ||
|
|
ffedd02b08 | ||
|
|
c84684a425 | ||
|
|
aed560339b | ||
|
|
0612f4d0e0 | ||
|
|
ce621ee5d6 | ||
|
|
9c4bb5a244 | ||
|
|
c076f73a87 | ||
|
|
36265fcedf | ||
|
|
53419180be | ||
|
|
c5bd09702a | ||
|
|
fcb205a842 | ||
|
|
4323803fd6 | ||
|
|
903b8ff438 | ||
|
|
b1fd13bbcb | ||
|
|
878d19beb8 | ||
|
|
96ed1e33e3 | ||
|
|
374a0f9ce3 | ||
|
|
580bd5aeaa | ||
|
|
c359d6a97d | ||
|
|
038702a2a0 | ||
|
|
6426d40825 | ||
|
|
bbe102dd57 | ||
|
|
65484bc432 | ||
|
|
54f6cc7a64 | ||
|
|
f1b2338227 | ||
|
|
45defebcf4 | ||
|
|
86ee8273bc | ||
|
|
3adfeb3b34 | ||
|
|
9bb8a26706 | ||
|
|
54b7f7127c | ||
|
|
25609db567 | ||
|
|
2e3603507c | ||
|
|
2efc1b5a87 | ||
|
|
090c67138a | ||
|
|
d8f387f796 | ||
|
|
aaeffe925e | ||
|
|
f814dd03eb | ||
|
|
2369ce5554 | ||
|
|
c19479757a |
@@ -1,4 +1,7 @@
|
||||
files/
|
||||
dist/
|
||||
logs/
|
||||
|
||||
Dockerfile
|
||||
docker-manifest.tmpl
|
||||
docker-manifest-unstable.tmpl
|
||||
|
||||
+101
-248
@@ -1,5 +1,6 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: testing
|
||||
|
||||
workspace:
|
||||
@@ -111,7 +112,7 @@ steps:
|
||||
# compiling the same magefile at the same time. It's also faster if each step does not need to compile it first.
|
||||
- name: mage
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
commands:
|
||||
@@ -122,7 +123,7 @@ steps:
|
||||
|
||||
- name: build
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
depends_on: [ mage ]
|
||||
@@ -132,21 +133,22 @@ steps:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: lint
|
||||
image: golang:1.17-alpine
|
||||
pull: true
|
||||
image: golang:1.19-alpine
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
depends_on: [ build ]
|
||||
commands:
|
||||
- export "GOROOT=$(go env GOROOT)"
|
||||
- apk --no-cache add build-base git
|
||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.47.3
|
||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.51.2
|
||||
- ./mage-static check:all
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test-migration-prepare
|
||||
image: kolaente/toolbox:latest
|
||||
pull: true
|
||||
pull: always
|
||||
commands:
|
||||
# Get the latest version
|
||||
- wget https://dl.vikunja.io/api/unstable/vikunja-unstable-linux-amd64-full.zip -q -O vikunja-latest.zip
|
||||
@@ -154,7 +156,7 @@ steps:
|
||||
|
||||
- name: test-migration-sqlite
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
@@ -173,7 +175,7 @@ steps:
|
||||
|
||||
- name: test-migration-mysql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
@@ -192,7 +194,7 @@ steps:
|
||||
|
||||
- name: test-migration-psql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: postgres
|
||||
@@ -212,7 +214,7 @@ steps:
|
||||
|
||||
- name: test
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
commands:
|
||||
@@ -223,7 +225,7 @@ steps:
|
||||
|
||||
- name: test-sqlite
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
@@ -240,7 +242,7 @@ steps:
|
||||
|
||||
- name: test-mysql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
@@ -257,7 +259,7 @@ steps:
|
||||
|
||||
- name: test-postgres
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
@@ -275,7 +277,7 @@ steps:
|
||||
|
||||
- name: integration-test
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
commands:
|
||||
@@ -286,7 +288,7 @@ steps:
|
||||
|
||||
- name: integration-test-sqlite
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
@@ -303,7 +305,7 @@ steps:
|
||||
|
||||
- name: integration-test-mysql
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
@@ -320,7 +322,7 @@ steps:
|
||||
|
||||
- name: integration-test-postgres
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
@@ -342,14 +344,15 @@ steps:
|
||||
########
|
||||
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release
|
||||
|
||||
depends_on:
|
||||
- testing
|
||||
|
||||
workspace:
|
||||
base: /go
|
||||
path: src/code.vikunja.io/api
|
||||
base: /source
|
||||
path: /
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
@@ -367,7 +370,7 @@ steps:
|
||||
# compiling the same magefile at the same time. It's also faster if each step does not need to compile it first.
|
||||
- name: mage
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
commands:
|
||||
@@ -377,7 +380,7 @@ steps:
|
||||
|
||||
- name: before-static-build
|
||||
image: techknowlogick/xgo:latest
|
||||
pull: true
|
||||
pull: always
|
||||
commands:
|
||||
- export PATH=$PATH:$GOPATH/bin
|
||||
- go install github.com/magefile/mage
|
||||
@@ -386,7 +389,7 @@ steps:
|
||||
|
||||
- name: static-build-windows
|
||||
image: techknowlogick/xgo:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
|
||||
# Leaving this here until we know how to resolve this properly.
|
||||
@@ -399,7 +402,7 @@ steps:
|
||||
|
||||
- name: static-build-linux
|
||||
image: techknowlogick/xgo:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
|
||||
# Leaving this here until we know how to resolve this properly.
|
||||
@@ -412,7 +415,7 @@ steps:
|
||||
|
||||
- name: static-build-darwin
|
||||
image: techknowlogick/xgo:latest
|
||||
pull: true
|
||||
pull: always
|
||||
environment:
|
||||
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
|
||||
# Leaving this here until we know how to resolve this properly.
|
||||
@@ -425,7 +428,7 @@ steps:
|
||||
|
||||
- name: after-build-compress
|
||||
image: kolaente/upx
|
||||
pull: true
|
||||
pull: always
|
||||
depends_on:
|
||||
- static-build-windows
|
||||
- static-build-linux
|
||||
@@ -435,7 +438,7 @@ steps:
|
||||
|
||||
- name: after-build-static
|
||||
image: techknowlogick/xgo:latest
|
||||
pull: true
|
||||
pull: always
|
||||
depends_on:
|
||||
- after-build-compress
|
||||
commands:
|
||||
@@ -447,7 +450,7 @@ steps:
|
||||
|
||||
- name: sign-release
|
||||
image: plugins/gpgsign:1
|
||||
pull: true
|
||||
pull: always
|
||||
depends_on: [ after-build-static ]
|
||||
settings:
|
||||
key:
|
||||
@@ -461,7 +464,7 @@ steps:
|
||||
# Push the releases to our pseudo-s3-bucket
|
||||
- name: release-latest
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
@@ -483,7 +486,7 @@ steps:
|
||||
|
||||
- name: release-version
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
@@ -502,21 +505,40 @@ steps:
|
||||
depends_on: [ sign-release ]
|
||||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages
|
||||
image: goreleaser/nfpm
|
||||
pull: true
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm:v2.26.0
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
- ./mage-static release:packages
|
||||
- mv dist/os-packages/vikunja*.x86_64.rpm dist/os-packages/vikunja-unstable-x86_64.rpm
|
||||
- mv dist/os-packages/vikunja*_amd64.deb dist/os-packages/vikunja-unstable-amd64.deb
|
||||
- mv dist/os-packages/vikunja*_x86_64.apk dist/os-packages/vikunja-unstable-x86_64.apk
|
||||
depends_on: [ static-build-linux ]
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on: [ after-build-compress ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm:v2.26.0
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
- ./mage-static release:packages
|
||||
- mv dist/os-packages/vikunja*.x86_64.rpm dist/os-packages/vikunja-${DRONE_TAG##v}-x86_64.rpm
|
||||
- mv dist/os-packages/vikunja*_amd64.deb dist/os-packages/vikunja-${DRONE_TAG##v}-amd64.deb
|
||||
- mv dist/os-packages/vikunja*_x86_64.apk dist/os-packages/vikunja-${DRONE_TAG##v}-x86_64.apk
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
depends_on: [ after-build-compress ]
|
||||
|
||||
# Push the os releases to our pseudo-s3-bucket
|
||||
- name: release-os-latest
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
@@ -534,11 +556,11 @@ steps:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on: [ build-os-packages ]
|
||||
depends_on: [ build-os-packages-unstable ]
|
||||
|
||||
- name: release-os-version
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
@@ -554,47 +576,11 @@ steps:
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
depends_on: [ build-os-packages ]
|
||||
|
||||
### Broken, disabled until we figure out how to fix it
|
||||
# - name: deb-structure
|
||||
# image: kolaente/reprepro
|
||||
# pull: true
|
||||
# environment:
|
||||
# GPG_PRIVATE_KEY:
|
||||
# from_secret: gpg_privatekey
|
||||
# commands:
|
||||
# - export GPG_TTY=$(tty)
|
||||
# - gpg -qk
|
||||
# - echo "use-agent" >> ~/.gnupg/gpg.conf
|
||||
# - gpgconf --kill gpg-agent
|
||||
# - echo $GPG_PRIVATE_KEY > ~/frederik.gpg
|
||||
# - gpg --import ~/frederik.gpg
|
||||
# - mkdir debian/conf -p
|
||||
# - cp build/reprepro-dist-conf debian/conf/distributions
|
||||
# - ./mage-static release:reprepro
|
||||
# depends_on: [ build-os-packages ]
|
||||
|
||||
# Push the releases to our pseudo-s3-bucket
|
||||
- name: release-deb
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
path_style: true
|
||||
strip_prefix: debian
|
||||
source: debian/*/*/*/*/*
|
||||
target: /deb/
|
||||
# depends_on: [ deb-structure ]
|
||||
depends_on: [ build-os-packages-version ]
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: deploy-docs
|
||||
|
||||
workspace:
|
||||
@@ -613,8 +599,7 @@ trigger:
|
||||
steps:
|
||||
- name: theme
|
||||
image: kolaente/toolbox
|
||||
pull: true
|
||||
group: build-static
|
||||
pull: always
|
||||
commands:
|
||||
- mkdir docs/themes/vikunja -p
|
||||
- cd docs/themes/vikunja
|
||||
@@ -622,8 +607,8 @@ steps:
|
||||
- tar -xzf vikunja-theme.tar.gz
|
||||
|
||||
- name: build
|
||||
image: klakegg/hugo:0.93.3
|
||||
pull: true
|
||||
image: klakegg/hugo:0.107.0
|
||||
pull: always
|
||||
commands:
|
||||
- cd docs
|
||||
- hugo
|
||||
@@ -631,7 +616,7 @@ steps:
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
pull: true
|
||||
pull: always
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
@@ -644,102 +629,11 @@ steps:
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-arm-release
|
||||
name: docker-release
|
||||
|
||||
depends_on:
|
||||
- testing
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
- "refs/tags/**"
|
||||
|
||||
steps:
|
||||
- name: fetch-tags
|
||||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
|
||||
- name: docker-arm-unstable
|
||||
image: plugins/docker:linux-arm
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: unstable-linux-arm
|
||||
dockerfile: Dockerfile.arm32
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
|
||||
- name: docker-arm
|
||||
image: plugins/docker:linux-arm
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm
|
||||
dockerfile: Dockerfile.arm32
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
- name: docker-arm64-unstable
|
||||
image: plugins/docker:linux-arm64
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: unstable-linux-arm64
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
|
||||
- name: docker-arm64
|
||||
image: plugins/docker:linux-arm64
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm64
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-amd64-release
|
||||
|
||||
depends_on:
|
||||
- testing
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
@@ -752,94 +646,55 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: docker-unstable
|
||||
image: plugins/docker:linux-amd64
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: unstable-linux-amd64
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker:linux-amd64
|
||||
pull: true
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-amd64
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: docker-manifest
|
||||
|
||||
trigger:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
- "refs/tags/**"
|
||||
|
||||
depends_on:
|
||||
- docker-amd64-release
|
||||
- docker-arm-release
|
||||
|
||||
steps:
|
||||
- name: manifest-unstable
|
||||
image: thegeeklab/drone-docker-buildx
|
||||
privileged: true
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: unstable
|
||||
ignore_missing: true
|
||||
spec: docker-manifest-unstable.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64/v8
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
|
||||
- name: manifest-release
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
spec: docker-manifest.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
- name: generate-tags
|
||||
image: thegeeklab/docker-autotag
|
||||
environment:
|
||||
DOCKER_AUTOTAG_VERSION: ${DRONE_TAG}
|
||||
DOCKER_AUTOTAG_EXTRA_TAGS: latest
|
||||
DOCKER_AUTOTAG_OUTPUT_FILE: .tags
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
- name: manifest-release-latest
|
||||
- name: docker-release
|
||||
image: thegeeklab/drone-docker-buildx
|
||||
privileged: true
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
depends_on:
|
||||
- clone
|
||||
settings:
|
||||
tags: latest
|
||||
ignore_missing: true
|
||||
spec: docker-manifest.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
- linux/arm64/v8
|
||||
depends_on: [ generate-tags ]
|
||||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
@@ -858,9 +713,7 @@ depends_on:
|
||||
- testing
|
||||
- release
|
||||
- deploy-docs
|
||||
- docker-arm-release
|
||||
- docker-amd64-release
|
||||
- docker-manifest
|
||||
- docker-release
|
||||
|
||||
steps:
|
||||
- name: notify
|
||||
@@ -878,6 +731,6 @@ steps:
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: e72b631f902689777e3263ae9527e5aa47738b9021538f7cb5034f95ac265f07
|
||||
hmac: 7242860ad70556ffeb8fc804ce0ffa0d3d1aa8e0d9167ad476aa392d7e937d48
|
||||
|
||||
...
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<!--
|
||||
|
||||
Please fill out this issue template to report a bug.
|
||||
If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
||||
|
||||
-->
|
||||
|
||||
**Version information:**
|
||||
|
||||
Frontend Version:
|
||||
API Version:
|
||||
Browser and OS Version:
|
||||
|
||||
**Steps to reproduce:**
|
||||
|
||||
<!--
|
||||
Add clear steps to reproduce the bug. Provide screenshots where applicable.
|
||||
-->
|
||||
|
||||
1.
|
||||
2.
|
||||
...
|
||||
|
||||
**Expected behavior:**
|
||||
|
||||
<!--
|
||||
Describe what happened.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
**Actual behavior:**
|
||||
|
||||
<!--
|
||||
Describe what happened instead.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
**Checklist:**
|
||||
|
||||
* [ ] I have provided all required information
|
||||
* [ ] I am using the latest release or the latest unstable build
|
||||
* [ ] I was able to reproduce the bug on [try](https://try.vikunja.io)
|
||||
@@ -1,11 +0,0 @@
|
||||
# Description
|
||||
|
||||
|
||||
|
||||
# Checklist
|
||||
|
||||
* [ ] I added or improved tests
|
||||
* [ ] I added or improved docs for my feature
|
||||
* [ ] Swagger (including `mage do-the-swag`)
|
||||
* [ ] Error codes
|
||||
* [ ] New config options (including adding them to `config.yml.saml` and running `mage generate-docs`)
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
github: kolaente
|
||||
custom: https://www.buymeacoffee.com/kolaente
|
||||
open_collective: vikunja
|
||||
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Bug Report
|
||||
description: Found something you weren't expecting? Report it here!
|
||||
labels: kind/bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
NOTE: If your issue is a security concern, please send an email to security@vikunja.io instead of opening a public issue.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill out this issue template to report a bug.
|
||||
|
||||
1. If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
|
||||
2. Please ask questions or configuration/deploy problems on our [Matrix Room](https://matrix.to/#/#vikunja:matrix.org) or forum (https://community.vikunja.io).
|
||||
3. Make sure you are using the latest release and
|
||||
take a moment to check that your issue hasn't been reported before.
|
||||
4. Please give all relevant information below for bug reports, because
|
||||
incomplete details will be handled as an invalid report and closed.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below).
|
||||
- type: input
|
||||
id: frontend-version
|
||||
attributes:
|
||||
label: Vikunja Frontend Version
|
||||
description: Vikunja frontend version (or commit reference) of your instance
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: api-version
|
||||
attributes:
|
||||
label: Vikunja API Version
|
||||
description: Vikunja API version (or commit reference) of your instance
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: browser-version
|
||||
attributes:
|
||||
label: Browser and version
|
||||
description: If your issue is related to a frontend problem, please provide the browser and version you used to reproduce it.
|
||||
- type: dropdown
|
||||
id: can-reproduce
|
||||
attributes:
|
||||
label: Can you reproduce the bug on the Vikunja demo site?
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If this issue involves the Web Interface, please provide one or more screenshots
|
||||
@@ -0,0 +1,17 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Frontend issues
|
||||
url: https://code.vikunja.io/frontend/issues
|
||||
about: This is the API repo. Please open frontend-related bug reports and discussions in the frontend repo. Not sure if you issue is frontend or api? Ask in Matrix or the forum first.
|
||||
- name: Forum
|
||||
url: https://community.vikunja.io/
|
||||
about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum.
|
||||
- name: Security-related issues
|
||||
url: https://vikunja.io/contact/#security
|
||||
about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue.
|
||||
- name: Chat on Matrix
|
||||
url: https://matrix.to/#/#vikunja:matrix.org
|
||||
about: Please ask any quick questions here.
|
||||
- name: Translations
|
||||
url: https://crowdin.com/project/vikunja
|
||||
about: Any problems or requests for new languages about translations should be handled in crowdin.
|
||||
@@ -4,6 +4,8 @@
|
||||
config.yml
|
||||
config.yaml
|
||||
!docs/config.yml
|
||||
!.github/ISSUE_TEMPLATE/config.yml
|
||||
!.gitea/ISSUE_TEMPLATE/config.yml
|
||||
docs/themes/
|
||||
*.db
|
||||
Run
|
||||
|
||||
@@ -88,3 +88,6 @@ issues:
|
||||
- path: pkg/models/favorites\.go
|
||||
linters:
|
||||
- nilerr
|
||||
- path: pkg/models/events\.go
|
||||
linters:
|
||||
- musttag
|
||||
|
||||
+399
@@ -7,6 +7,405 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
All releases can be found on https://code.vikunja.io/api/releases.
|
||||
|
||||
## [0.20.3] - 2023-03-10
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(build)* Downgrade xgo to 1.19.2 so that builds work again
|
||||
* *(caldav)* Add Z suffix to dates make it clear dates are in UTC
|
||||
* *(caldav)* Use const for repeat modes
|
||||
* *(caldav)* Make sure only labels where the user has permission to use them are used
|
||||
* *(ci)* Pipeline dependency
|
||||
* *(ci)* Pin nfpm container version and binary location
|
||||
* *(ci)* Set release path to /source
|
||||
* *(ci)* Tagging logic for release docker images
|
||||
* *(ci)* Save generated .tags file to correctly tag docker releases
|
||||
* *(ci)* Sign drone config
|
||||
* *(docd)* Update Subdirectory Documentation (#1363)
|
||||
* *(docker)* Cross compilation with buildx
|
||||
* *(docker)* Re-add expose
|
||||
* *(docker)* Passing environment variables into the container
|
||||
* *(docker)* Make sure the vikunja user always exists and only modify the uid instead of recreating the user
|
||||
* *(docs)* Add docs about cli user delete
|
||||
* *(docs)* Old helm charts url (#1344)
|
||||
* *(docs)* Fix a few minor typos (#59)
|
||||
* *(docs)* Fix traefik v2 example (#65)
|
||||
* *(docs)* Clarify support for caldav reccurrence
|
||||
* *(drone)* Add type, fix pull, remove group (#1355)
|
||||
* *(dump)* Make sure null dates are properly set when restoring from a dump
|
||||
* *(export)* Ignore file size for export files
|
||||
* *(list)* Return lists for a namespace id even if that namespace is deleted
|
||||
* *(list)* When list background is removed, delete file from file system and DB (#1372)
|
||||
* *(mailer)* Forcessl config (#60)
|
||||
* *(migration)* Use Todoist v9 api to migrate tasks from them
|
||||
* *(migration)* Import TickTick data by column name instead of index (#1356)
|
||||
* *(migration)* Use the proper authorization method for Todoist's api, fix issues with importing deleted items
|
||||
* *(migration)* Remove unused todoist parameters
|
||||
* *(migration)* Todoist pagination now avoids too many loops
|
||||
* *(migration)* Don't try to add nonexistent tasks as related
|
||||
* *(migration)* Make sure trello checklists are properly imported
|
||||
* *(reminders)* Overdue tasks join condition
|
||||
* *(reminders)* Make sure an overdue reminder is sent when there is only one overdue task
|
||||
* *(reminders)* Prevent duplicate reminders when updating task details
|
||||
* *(restore)* Check if we're really dealing with a string
|
||||
* *(task)* Make sure the task's last updated timestamp is always updated when releated entities changed
|
||||
* *(task)* Correctly load tasks by id and uuid in caldav
|
||||
* *(tasks)* Don't include undone overdue tasks from archived lists or namespaces in notification mails
|
||||
* *(tasks)* Don't reset the kanban bucket when updating a task and not providing one
|
||||
* *(tasks)* Don't set a repeating task done when moving it do the done bucket
|
||||
* *(tasks)* Recalculate position of all tasks in a list or bucket when it would hit 0
|
||||
* *(tasks)* Make sure tasks are sorted by position before recalculating them
|
||||
* *(user)* Make reset the user's name to empty actually work
|
||||
* Swagger docs ([96b5e93](96b5e933796275e87f3007e31db0623688dbdb3a))
|
||||
* Restore notifications table from dump when it already had the correct format ([8c67be5](8c67be558f697ab52740c51ab453092c0f8f7c14))
|
||||
* Make sure labels are always exported as caldav (#1412) ([1afc72e](1afc72e1906c02b093bb6d9748235b93ab0eb181))
|
||||
* Lint ([491a142](491a1423788b76f236d070071cb46f5b2f5d3fd0))
|
||||
* Lint ([20a5994](20a5994b1717e7751750f14a9a164825a8e6ade6))
|
||||
* Lint ([077baba](077baba2eaff2f10b97384f07375ece7f51ec0fa))
|
||||
* Lint ([9f14466](9f14466dfa8660362a4e51b3c8c6810bf8d66a22))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.5.3 (#1317)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.2.0 (#1315)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.9.3 (#1320)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.3.0 (#1321)
|
||||
* *(deps)* Update github.com/arran4/golang-ical digest to a677353 (#1323)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.5 (#1325)
|
||||
* *(deps)* Update github.com/arran4/golang-ical digest to 1093469 (#1326)
|
||||
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.4.3 (#1328)
|
||||
* *(deps)* Update module github.com/go-sql-driver/mysql to v1.7.0 (#1332)
|
||||
* *(deps)* Update module golang.org/x/sys to v0.3.0 (#1333)
|
||||
* *(deps)* Update module golang.org/x/term to v0.3.0 (#1336)
|
||||
* *(deps)* Update module golang.org/x/image to v0.2.0 (#1335)
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.2.0 (#1316)
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.3.0 (#1337)
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.16.0 (#1338)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.4.0 (#1339)
|
||||
* *(deps)* Update module github.com/pquerna/otp to v1.4.0 (#1341)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.9 (#1327)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.6 (#1342)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.0 (#1343)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.7 (#1348)
|
||||
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.5.0 (#1349)
|
||||
* *(deps)* Update module golang.org/x/sys to v0.4.0 (#1351)
|
||||
* *(deps)* Update module golang.org/x/image to v0.3.0 (#1350)
|
||||
* *(deps)* Update module golang.org/x/term to v0.4.0 (#1352)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.5.0 (#1353)
|
||||
* *(deps)* Update goreleaser/nfpm docker tag to v2.23.0 (#1347)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.8 (#1357)
|
||||
* *(deps)* Update module src.techknowlogick.com/xgo to v1.6.0+1.19.5 (#1358)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.107.0 (#1272)
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.17.0 (#1361)
|
||||
* *(deps)* Update module src.techknowlogick.com/xgo to v1.7.0+1.19.5 (#1364)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.15.0 (#1365)
|
||||
* *(deps)* Update module github.com/labstack/echo-jwt/v4 to v4.0.1 (#1369)
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.4.0 (#1354)
|
||||
* *(deps)* Update github.com/gocarina/gocsv digest to 763e25b (#1370)
|
||||
* *(deps)* Update goreleaser/nfpm docker tag to v2.24.0 (#1367)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.10 (#1371)
|
||||
* *(deps)* Update module github.com/go-redis/redis/v8 to v9 (#1377)
|
||||
* *(deps)* Update module github.com/labstack/echo-jwt/v4 to v4.1.0
|
||||
* *(deps)* Update module github.com/ulule/limiter/v3 to v3.11.0 (#1378)
|
||||
* *(deps)* Update module github.com/redis/go-redis/v9 to v9.0.2
|
||||
* *(deps)* Update goreleaser/nfpm docker tag to v2.25.0 (#1382)
|
||||
* *(deps)* Upgrade golangci-lint to 1.51.0
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.5.4
|
||||
* *(deps)* Update module go to 1.20
|
||||
* *(deps)* Update xgo to 1.20
|
||||
* *(deps)* Update module golang.org/x/sys to v0.5.0
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.18.0 (#1386)
|
||||
* *(deps)* Update module golang.org/x/term to v0.5.0
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.6.0
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.5.0
|
||||
* *(deps)* Update module golang.org/x/image to v0.4.0
|
||||
* *(deps)* Update goreleaser/nfpm docker tag to v2.26.0 (#1394)
|
||||
* *(deps)* Update github.com/arran4/golang-ical digest to 07c6aad
|
||||
* *(deps)* Update module github.com/threedotslabs/watermill to v1.2.0 (#1384)
|
||||
* *(deps)* Update module golang.org/x/image to v0.5.0 (#1396)
|
||||
* *(deps)* Update golang.org/x/net to 0.7.0
|
||||
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.5.0 (#1399)
|
||||
* *(deps)* Update github.com/gocarina/gocsv digest to bcce7dc
|
||||
* *(deps)* Update golangci-lint to 1.51.2
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.1
|
||||
* *(deps)* Update github.com/gocarina/gocsv digest to bee85ea
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.2
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.9.4
|
||||
* *(deps)* Update github.com/gocarina/gocsv digest to dc4ee9d
|
||||
* *(deps)* Update module github.com/stretchr/testify to v1.8.2
|
||||
* *(deps)* Update github.com/gocarina/gocsv digest to 70c27cb
|
||||
* *(deps)* Update module golang.org/x/sys to v0.6.0
|
||||
* *(deps)* Update module golang.org/x/term to v0.6.0
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.7.0
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.6.0
|
||||
* *(deps)* Update module golang.org/x/image to v0.6.0
|
||||
* *(deps)* Update github.com/kolaente/caldav-go digest to 2a4eb8b
|
||||
* *(deps)* Remove fsnotify replacement
|
||||
* *(deps)* Update github.com/vectordotdev/go-datemath digest to f3954d0
|
||||
* *(deps)* Update src.techknowlogick.com/xgo digest to 44f7e66
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.19.0
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.9.5
|
||||
* *(deps)* Update module github.com/ulule/limiter/v3 to v3.11.1
|
||||
* *(deps)* Update src.techknowlogick.com/xgo digest to b607086
|
||||
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.2
|
||||
|
||||
### Features
|
||||
|
||||
* *(background)* Add Last-Modified header (#1376)
|
||||
* *(caldav)* Add support for repeating tasks
|
||||
* *(caldav)* Export Labels to Caldav (#1409)
|
||||
* *(caldav)* Import caldav categories as Labels (#1413)
|
||||
* *(migrators)* Remove wunderlist (#1346)
|
||||
* *(release)* Use compressed binaries for package releases
|
||||
* Use docker buildx to build multiarch images ([a6e214b](a6e214b654f28836cc8b93683dbfd5999282d11c))
|
||||
* Provide logout url for openid providers (#1340) ([a79b1de](a79b1de2d0247a424f49cecaa267d30e8fa70a83))
|
||||
* Refactored Dockerfile (#1375) ([522bf7d](522bf7d2fc3cc1704f58299b6435baccc7add533))
|
||||
* Disable events log by default ([da9d25c](da9d25cf727c56acd7394b4b74e17a2959ee5242))
|
||||
- **BREAKING**: events log level is now off unless explicitly enabled
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(docs)* Adjust docs about frontend docker container
|
||||
* *(docs)* Remove sponsors
|
||||
* *(task)* Add test to check if a task's reminders are duplicated
|
||||
* Remove custom gitea bug template in favor of githubs ([4fa45bf](4fa45bf9dcbaa8a41a53fc2305c4c2c1aa15691c))
|
||||
* 0.20.2 release preperations ([d19fc80](d19fc80b8be08673136d84e10187cadb293822bf))
|
||||
* Update funding links ([aa25ccd](aa25ccdc917684583a9bff4b7cb272004386f0fa))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* Added Google & Google Workspace to OpenId examples (#1319)
|
||||
|
||||
|
||||
## [0.20.2] - 2023-01-24
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(build)* Downgrade xgo to 1.19.2 so that builds work again
|
||||
* *(caldav)* Add Z suffix to dates make it clear dates are in UTC
|
||||
* *(caldav)* Use const for repeat modes
|
||||
* *(ci)* Pipeline dependency
|
||||
* *(ci)* Pin nfpm container version and binary location
|
||||
* *(ci)* Set release path to /source
|
||||
* *(ci)* Tagging logic for release docker images
|
||||
* *(docs)* Add docs about cli user delete
|
||||
* *(docs)* Old helm charts url (#1344)
|
||||
* *(docs)* Fix a few minor typos (#59)
|
||||
* *(drone)* Add type, fix pull, remove group (#1355)
|
||||
* *(dump)* Make sure null dates are properly set when restoring from a dump
|
||||
* *(export)* Ignore file size for export files
|
||||
* *(list)* Return lists for a namespace id even if that namespace is deleted
|
||||
* *(mailer)* Forcessl config (#60)
|
||||
* *(migration)* Use Todoist v9 api to migrate tasks from them
|
||||
* *(migration)* Import TickTick data by column name instead of index (#1356)
|
||||
* *(migration)* Use the proper authorization method for Todoist's api, fix issues with importing deleted items
|
||||
* *(reminders)* Overdue tasks join condition
|
||||
* *(reminders)* Make sure an overdue reminder is sent when there is only one overdue task
|
||||
* *(reminders)* Prevent duplicate reminders when updating task details
|
||||
* *(restore)* Check if we're really dealing with a string
|
||||
* *(tasks)* Don't include undone overdue tasks from archived lists or namespaces in notification mails
|
||||
* *(tasks)* Don't reset the kanban bucket when updating a task and not providing one
|
||||
* *(tasks)* Don't set a repeating task done when moving it do the done bucket
|
||||
* *(user)* Make reset the user's name to empty actually work* Swagger docs ([41c9e3f](41c9e3f9a47280887b56941280904aea6ef31f85))
|
||||
* Restore notifications table from dump when it already had the correct format ([15811fd](15811fd4d4485cd25cf8d2f8fdd04ebfea8e6663))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.5.3 (#1317)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.2.0 (#1315)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.9.3 (#1320)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.3.0 (#1321)
|
||||
* *(deps)* Update github.com/arran4/golang-ical digest to a677353 (#1323)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.5 (#1325)
|
||||
* *(deps)* Update github.com/arran4/golang-ical digest to 1093469 (#1326)
|
||||
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.4.3 (#1328)
|
||||
* *(deps)* Update module github.com/go-sql-driver/mysql to v1.7.0 (#1332)
|
||||
* *(deps)* Update module golang.org/x/sys to v0.3.0 (#1333)
|
||||
* *(deps)* Update module golang.org/x/term to v0.3.0 (#1336)
|
||||
* *(deps)* Update module golang.org/x/image to v0.2.0 (#1335)
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.2.0 (#1316)
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.3.0 (#1337)
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.16.0 (#1338)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.4.0 (#1339)
|
||||
* *(deps)* Update module github.com/pquerna/otp to v1.4.0 (#1341)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.9 (#1327)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.6 (#1342)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.10.0 (#1343)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.7 (#1348)
|
||||
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.5.0 (#1349)
|
||||
* *(deps)* Update module golang.org/x/sys to v0.4.0 (#1351)
|
||||
* *(deps)* Update module golang.org/x/image to v0.3.0 (#1350)
|
||||
* *(deps)* Update module golang.org/x/term to v0.4.0 (#1352)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.5.0 (#1353)
|
||||
* *(deps)* Update goreleaser/nfpm docker tag to v2.23.0 (#1347)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.8 (#1357)
|
||||
* *(deps)* Update module src.techknowlogick.com/xgo to v1.6.0+1.19.5 (#1358)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.107.0 (#1272)
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.17.0 (#1361)
|
||||
* *(deps)* Update module src.techknowlogick.com/xgo to v1.7.0+1.19.5 (#1364)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.15.0 (#1365)
|
||||
* *(deps)* Update module github.com/labstack/echo-jwt/v4 to v4.0.1 (#1369)
|
||||
|
||||
### Features
|
||||
|
||||
* *(migrators)* Remove wunderlist (#1346)
|
||||
* *(release)* Use compressed binaries for package releases
|
||||
* Use docker buildx to build multiarch images ([9bd6795](9bd6795266fd54ae42664c20ed7633ac7daf6199))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* Remove custom gitea bug template in favor of githubs ([7b1e1c7](7b1e1c79e358f3fcecb217259491f016402cdcc7))
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* Added Google & Google Workspace to OpenId examples (#1319)
|
||||
|
||||
## [0.20.1] - 2022-11-11
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(docs)* Add explanation on how to run the cli in docker
|
||||
* *(filter)* Also check for 0 values if the filter should include nulls
|
||||
* *(filter)* Only check for 0 values in filter fields with numeric values
|
||||
* *(filters)* Try to parse date filter fields of the provided dates are not valid iso dates
|
||||
* *(filters)* Try parsing dates without time
|
||||
* *(filters)* Try parsing invalid dates like 2022-11-1
|
||||
* *(metrics)* Make currently active users actually work
|
||||
* *(task)* Duplicate reminders when adding different ones between winter / summer time
|
||||
* *(tasks)* Allow sorting by task index* Make sure task indexes are calculated correctly when moving tasks between lists ([c495096](c4950964443a9bffc4cdd8fc25004ad951520f20))
|
||||
* Look for the default bucket based on the position instead of the index ([622f2f0](622f2f0562bd8e3a5c97ec0b001c646a33a86c2b))
|
||||
* Usage with postgres over unix socket (#1308) ([641a9da](641a9da93d24a18d6cbad2929eea1be6c1e0d0b2))
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.13.1 (#1307)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.14.0 (#1309)
|
||||
* *(deps)* Update module golang.org/x/sys to v0.2.0 (#1311)
|
||||
* *(deps)* Update module golang.org/x/term to v0.2.0 (#1312)
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.14.0 (#1313)
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.15.0 (#1314)
|
||||
|
||||
### Features
|
||||
|
||||
* *(docs)* Add relase checklist
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* Nessecary is a common misspelling of necessary (#1304)
|
||||
|
||||
## [0.20.0] - 2022-10-28
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(caldav)* Make sure duration and due date follow rfc5545
|
||||
* *(caldav)* No failed login emails for tokens (#1252)
|
||||
* *(ci)* Make sure release zip files have a .zip ending
|
||||
* *(ci)* Make sure release os packages are properly named
|
||||
* *(docs)* Clarify using port 25 as mail port when mail does not work
|
||||
* *(docs)* Document pnpm instead of yarn
|
||||
* *(docs)* Fix redirect_url example (#50)
|
||||
* *(lists)* Return correct max right for lists where the user has created the namespace
|
||||
* *(mail)* Pass mail server timeout (#1253)
|
||||
* *(migration)* Properly parse duration
|
||||
* *(migration)* Expose ticktick migrator to /info
|
||||
* *(migration)* Make sure importing works when the csv file has errors and don't try to parse empty values as dates
|
||||
* *(namespaces)* Add list subscriptions (#1254)
|
||||
* *(todoist)* Properly import all done tasks* Properly log extra message ([c194797](c19479757a20d72484b4e071b45055746ff2b67e))
|
||||
* Don't try to compress riscv64 binaries in releases ([d8f387f](d8f387f7967ffb94035de2fcfc4578247ae1023e))
|
||||
* Preserve dates for repeating tasks (#47) ([090c671](090c67138a16258480b866b05c6fdc2e02d12c89))
|
||||
* Tasks with the same assignee as doer should not appear twice in overdue task mails ([45defeb](45defebcf435cade4b72763236e1e2dfdac770cc))
|
||||
* Don't allow setting a list namespace to 0 ([96ed1e3](96ed1e33e38beec1bb1ab0813074b035dd02fade))
|
||||
* Make sure pseudo namespaces and lists always have the current user as owner ([878d19b](878d19beb81869392e33a8ffc1ec247d1cf1e4d6))
|
||||
* Use connection string for postgres ([fcb205a](fcb205a842a4e828e6e933339b23f5aa8b297125))
|
||||
* Make sure user searches are always case-insensitive ([c076f73](c076f73a87bc9b39b17389e25d0186ab71aa24bf))
|
||||
* Make cover image id actually updatable ([0e1904d](0e1904d50b8576a2e9ea5812314aa3c8f304edb5))
|
||||
* Make cover image id actually updatable ([0eb4709](0eb47096db02ceb5032c7439b3b901fbadd0d1bb))
|
||||
* Make sure a user can only be assigned once to a task ([008908e](008908eb49eeb50a554c416422feb3b465efa165))
|
||||
* Make sure list subscriptions are set correctly when their namespace has a subscription already ([2fc690a](2fc690a783f5b702fad71da627aa616017727f56))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.101.0
|
||||
* *(deps)* Update golang.org/x/sync digest to 8fcdb60
|
||||
* *(deps)* Update golang.org/x/oauth2 digest to f213421
|
||||
* *(deps)* Update module src.techknowlogick.com/xgo to v1.5.0+1.19
|
||||
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.4.0
|
||||
* *(deps)* Update golang.org/x/image digest to e7cb969
|
||||
* *(deps)* Update golang.org/x/term digest to 7a66f97
|
||||
* *(deps)* Update module github.com/lib/pq to v1.10.7
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.13.0 (#1260)
|
||||
* *(deps)* Update dependency golang to v1.19 (#1228)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.2.8 (#1258)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.5.2 (#1261)
|
||||
* *(deps)* Update module src.techknowlogick.com/xormigrate to v1.5.0 (#1262)
|
||||
* *(deps)* Update module github.com/magefile/mage to v1.14.0 (#1259)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.6 (#1243)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.2.9 (#1264)
|
||||
* *(deps)* Update dependency klakegg/hugo to v0.102.3 (#1265)
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.14.0 (#1266)
|
||||
* *(deps)* Update module github.com/labstack/gommon to v0.4.0 (#1269)
|
||||
* *(deps)* Update golang.org/x/crypto digest to 4161e89 (#1268)
|
||||
* *(deps)* Update golang.org/x/oauth2 digest to b44042a (#1270)
|
||||
* *(deps)* Update golang.org/x/sys digest to 84dc82d (#1271)
|
||||
* *(deps)* Update dependency klakegg/hugo to v0.104.2 (#1267)
|
||||
* *(deps)* Update golang.org/x/crypto digest to d6f0a8c (#1275)
|
||||
* *(deps)* Update golang.org/x/sys digest to 090e330 (#1276)
|
||||
* *(deps)* Update module github.com/spf13/cobra to v1.6.0 (#1277)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.0 (#1278)
|
||||
* *(deps)* Update golang.org/x/crypto digest to 56aed06 (#1280)
|
||||
* *(deps)* Update golang.org/x/text to v0.3.8
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.1 (#1281)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.9.1 (#1282)
|
||||
* *(deps)* Update golang.org/x/sys digest to 95e765b (#1283)
|
||||
* *(deps)* Update golang.org/x/oauth2 digest to 6fdb5e3 (#1284)
|
||||
* *(deps)* Update golang.org/x/image digest to ffcb3fe (#1288)
|
||||
* *(deps)* Update module golang.org/x/sync to v0.1.0 (#1291)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.7 (#1290)
|
||||
* *(deps)* Update golang.org/x/term digest to 8365914 (#1289)
|
||||
* *(deps)* Update module github.com/coreos/go-systemd/v22 to v22.4.0 (#1287)
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.1.0 (#1296)
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.1.0 (#1295)
|
||||
* *(deps)* Update module golang.org/x/image to v0.1.0 (#1293)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.2 (#1297)
|
||||
* *(deps)* Update module github.com/stretchr/testify to v1.8.1 (#1298)
|
||||
* *(deps)* Update module github.com/spf13/cobra to v1.6.1 (#1299)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.3 (#1300)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.3.4 (#1302)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.16 (#1301)
|
||||
|
||||
### Features
|
||||
|
||||
* *(docs)* Add docs about how to deploy Vikunja in a subdirectory
|
||||
* *(docs)* Document pnpm (#1251)
|
||||
* *(migration)* Add TickTick migrator
|
||||
* *(migration)* Add routes for TickTick migrator
|
||||
* *(migration)* Generate swagger docs
|
||||
* *(task)* Add cover image attachment id property
|
||||
* *(task)* Add cover image attachment id property (#1263)* Add sponsor to readme (relm) ([f814dd0](f814dd03eb7f1ae08ea67ae0e3e89b8b4e684ce3))
|
||||
* Upgrade xorm ([b1fd13b](b1fd13bbcbc551d1bbfe78d91fe6209369709df5))
|
||||
* Upgrade xorm ([4323803](4323803fd6801e21121eac0d9f9cd62879f090f7))
|
||||
* Upgrade xorm (#1197) ([5341918](53419180be386d675b4513e7ec70aca85b5ac99b))
|
||||
* Add github issue templates ([9c4bb5a](9c4bb5a24429dec686e3ccdcd2b920ce5528031c))
|
||||
* Remove gitea issue template so that only the form is used ([ce621ee](ce621ee5d6b47a0776628073bbd53312a97d116b))
|
||||
* Add gitea issue template ([0612f4d](0612f4d0e03fbe85018f51056c4833557e78cd3f))
|
||||
* Provide default user settings for new users via config ([5a40100](5a40100ac5be33d2cbce3c25e355d4036b9b4d3f))
|
||||
* Add proper checks and errors to see if an attachment belongs to the task it's being used as cover image in ([631a265](631a265d2de9a6196faf28574023fc3cdcc0bfc7))
|
||||
* Allow a user to remove themselves from a team ([b8769c7](b8769c746ceddc9818f91d6a8a404293ea2e837e))
|
||||
* TickTick migrator (#1273) ([df2e36c](df2e36c2a378d4bd1b81d959da180b6e9b9a37b9))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* Upgrade echo ([86ee827](86ee8273bce36c7b4767a34e0d878d63b37ea1b4))
|
||||
* Go mod tidy ([903b8ff](903b8ff43871234f41f706d571ee2caaba5f4232))
|
||||
* Generate swagger docs ([e113fe3](e113fe34d074f698f4b0cb237821f359976daa5c))
|
||||
* Remove unused dependencies ([f5fd849](f5fd849a0b93ff3bba53ac4907bb3fb04fa8692b))
|
||||
|
||||
## [0.19.2] - 2022-08-17
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+26
-33
@@ -1,49 +1,42 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# ┬─┐┬ ┐o┬ ┬─┐
|
||||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
##############
|
||||
# Build stage
|
||||
FROM golang:1.18-alpine AS build-env
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.20.0 AS builder
|
||||
|
||||
RUN apk --no-cache add build-base git && \
|
||||
go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
RUN go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
|
||||
ARG VIKUNJA_VERSION
|
||||
|
||||
# Setup repo
|
||||
COPY . /go/src/code.vikunja.io/api
|
||||
WORKDIR /go/src/code.vikunja.io/api
|
||||
COPY . ./
|
||||
|
||||
# Checkout version if set
|
||||
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
|
||||
&& mage build:clean build
|
||||
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||
|
||||
RUN mage build:clean && \
|
||||
mage release:xgo "${TARGETOS}/${TARGETARCH}/${TARGETVARIANT}"
|
||||
|
||||
# ┬─┐┬ ┐┌┐┐┌┐┐┬─┐┬─┐
|
||||
# │┬┘│ │││││││├─ │┬┘
|
||||
# ┘└┘┘─┘┘└┘┘└┘┴─┘┘└┘
|
||||
|
||||
###################
|
||||
# The actual image
|
||||
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
|
||||
# because of this, the container would not start when I compiled the image without cgo.
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.16 AS runner
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
WORKDIR /app/vikunja
|
||||
ENTRYPOINT [ "/sbin/tini", "-g", "--", "/entrypoint.sh" ]
|
||||
EXPOSE 3456
|
||||
|
||||
WORKDIR /app/vikunja/
|
||||
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja .
|
||||
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
|
||||
|
||||
# Dynamic permission changing stuff
|
||||
ENV PUID 1000
|
||||
ENV PGID 1000
|
||||
RUN apk --no-cache add shadow && \
|
||||
addgroup -g ${PGID} vikunja && \
|
||||
adduser -s /bin/sh -D -G vikunja -u ${PUID} vikunja -h /app/vikunja -H && \
|
||||
chown vikunja -R /app/vikunja
|
||||
COPY run.sh /run.sh
|
||||
|
||||
# Add time zone data
|
||||
RUN apk --no-cache add tzdata
|
||||
RUN apk --update --no-cache add tzdata tini shadow && \
|
||||
addgroup vikunja && \
|
||||
adduser -s /bin/sh -D -G vikunja vikunja -h /app/vikunja -H
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod 0755 /entrypoint.sh && mkdir files
|
||||
|
||||
# Files permissions
|
||||
RUN mkdir /app/vikunja/files && \
|
||||
chown -R vikunja /app/vikunja/files
|
||||
VOLUME /app/vikunja/files
|
||||
|
||||
CMD ["/run.sh"]
|
||||
EXPOSE 3456
|
||||
COPY --from=builder /build/vikunja-* vikunja
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
|
||||
##############
|
||||
# Build stage
|
||||
FROM golang:1.18-buster AS build-env
|
||||
|
||||
RUN go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
|
||||
ARG VIKUNJA_VERSION
|
||||
|
||||
# Setup repo
|
||||
COPY . /go/src/code.vikunja.io/api
|
||||
WORKDIR /go/src/code.vikunja.io/api
|
||||
|
||||
# Checkout version if set
|
||||
RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
|
||||
&& mage build:clean build
|
||||
|
||||
###################
|
||||
# The actual image
|
||||
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
|
||||
# because of this, the container would not start when I compiled the image without cgo.
|
||||
# We're using debian as a base image here because the latest alpine image does not work with arm.
|
||||
FROM debian:buster-slim
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
|
||||
WORKDIR /app/vikunja/
|
||||
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja .
|
||||
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
|
||||
|
||||
# Dynamic permission changing stuff
|
||||
ENV PUID 1000
|
||||
ENV PGID 1000
|
||||
RUN addgroup --gid ${PGID} vikunja && \
|
||||
chown ${PUID} -R /app/vikunja && \
|
||||
useradd --shell /bin/sh --gid vikunja --uid ${PUID} --home-dir /app/vikunja vikunja
|
||||
COPY run.sh /run.sh
|
||||
|
||||
# Fix time zone settings not working
|
||||
RUN apt-get update && apt-get install -y tzdata && apt-get clean
|
||||
|
||||
# Files permissions
|
||||
RUN mkdir /app/vikunja/files && \
|
||||
chown -R vikunja /app/vikunja/files
|
||||
VOLUME /app/vikunja/files
|
||||
|
||||
CMD ["/run.sh"]
|
||||
EXPOSE 3456
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://drone.kolaente.de/vikunja/api)
|
||||
[](LICENSE)
|
||||
[](https://dl.vikunja.io)
|
||||
[](https://dl.vikunja.io)
|
||||
[](https://hub.docker.com/r/vikunja/api/)
|
||||
[](https://try.vikunja.io/api/v1/docs)
|
||||
[](https://goreportcard.com/report/kolaente.dev/vikunja/api)
|
||||
|
||||
+32
-18
@@ -127,7 +127,8 @@ mailer:
|
||||
enabled: false
|
||||
# SMTP Host
|
||||
host: ""
|
||||
# SMTP Host port
|
||||
# SMTP Host port.
|
||||
# **NOTE:** If you're unable to send mail and the only error you see in the logs is an `EOF`, try setting the port to `25`.
|
||||
port: 587
|
||||
# SMTP Auth Type. Can be either `plain`, `login` or `cram-md5`.
|
||||
authtype: "plain"
|
||||
@@ -161,10 +162,10 @@ log:
|
||||
databaselevel: "WARNING"
|
||||
# Whether to log http requests or not. Possible values are stdout, stderr, file or off to disable http logging.
|
||||
http: "stdout"
|
||||
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
|
||||
# Echo has its own logging which usually is unnecessary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
|
||||
echo: "off"
|
||||
# Whether or not to log events. Useful for debugging. Possible values are stdout, stderr, file or off to disable events logging.
|
||||
events: "stdout"
|
||||
events: "off"
|
||||
# The log level for event log messages. Possible values (case-insensitive) are ERROR, INFO, DEBUG.
|
||||
eventslevel: "info"
|
||||
|
||||
@@ -190,21 +191,6 @@ files:
|
||||
maxsize: 20MB
|
||||
|
||||
migration:
|
||||
# These are the settings for the wunderlist migrator
|
||||
wunderlist:
|
||||
# Wheter to enable the wunderlist migrator or not
|
||||
enable: false
|
||||
# The client id, required for making requests to the wunderlist api
|
||||
# You need to register your vikunja instance at https://developer.wunderlist.com/apps/new to get this
|
||||
clientid:
|
||||
# The client secret, also required for making requests to the wunderlist api
|
||||
clientsecret:
|
||||
# The url where clients are redirected after they authorized Vikunja to access their wunderlist stuff.
|
||||
# This needs to match the url you entered when registering your Vikunja instance at wunderlist.
|
||||
# This is usually the frontend url where the frontend then makes a request to /migration/wunderlist/migrate
|
||||
# with the code obtained from the wunderlist api.
|
||||
# Note that the vikunja frontend expects this to be /migrate/wunderlist
|
||||
redirecturl:
|
||||
todoist:
|
||||
# Wheter to enable the todoist migrator or not
|
||||
enable: false
|
||||
@@ -310,6 +296,9 @@ auth:
|
||||
- name:
|
||||
# The auth url to send users to if they want to authenticate using OpenID Connect.
|
||||
authurl:
|
||||
# The oidc logouturl that users will be redirected to on logout.
|
||||
# Leave empty or delete key, if you do not want to be redirected.
|
||||
logouturl:
|
||||
# The client ID used to authenticate Vikunja at the OpenID Connect provider.
|
||||
clientid:
|
||||
# The client secret used to authenticate Vikunja at the OpenID Connect provider.
|
||||
@@ -323,3 +312,28 @@ metrics:
|
||||
username:
|
||||
# If set to a non-empty value the /metrics endpoint will require this as a password via basic auth in combination with the username below.
|
||||
password:
|
||||
|
||||
# Provide default settings for new users. When a new user is created, these settings will automatically be set for the user. If you change them in the config file afterwards they will not be changed back for existing users.
|
||||
defaultsettings:
|
||||
# The avatar source for the user. Can be `gravatar`, `initials`, `upload` or `marble`. If you set this to `upload` you'll also need to specify `defaultsettings.avatar_file_id`.
|
||||
avatar_provider: initials
|
||||
# The id of the file used as avatar.
|
||||
avatar_file_id: 0
|
||||
# If set to true users will get task reminders via email.
|
||||
email_reminders_enabled: false
|
||||
# If set to true will allow other users to find this user when searching for parts of their name.
|
||||
discoverable_by_name: false
|
||||
# If set to true will allow other users to find this user when searching for their exact email.
|
||||
discoverable_by_email: false
|
||||
# If set to true will send an email every day with all overdue tasks at a configured time.
|
||||
overdue_tasks_reminders_enabled: true
|
||||
# When to send the overdue task reminder email.
|
||||
overdue_tasks_reminders_time: 9:00
|
||||
# The id of the default list. Make sure users actually have access to this list when setting this value.
|
||||
default_list_id: 0
|
||||
# Start of the week for the user. `0` is sunday, `1` is monday and so on.
|
||||
week_start: 0
|
||||
# The language of the user interface. Must be an ISO 639-1 language code. Will default to the browser language the user uses when signing up.
|
||||
language: <unset>
|
||||
# The time zone of each individual user. This will affect when users get reminders and overdue task emails.
|
||||
timezone: <time zone set at service.timezone>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
image: vikunja/api:unstable
|
||||
manifests:
|
||||
-
|
||||
image: vikunja/api:unstable-linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/api:unstable-linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/api:unstable-linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
@@ -1,23 +0,0 @@
|
||||
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
-
|
||||
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
if [ -n "$PUID" ] && [ "$PUID" -ne 0 ] && \
|
||||
[ -n "$PGID" ] && [ "$PGID" -ne 0 ] ; then
|
||||
echo "info: creating the new user vikunja with $PUID:$PGID"
|
||||
groupmod -g "$PGID" vikunja
|
||||
usermod -u "$PUID" vikunja
|
||||
chown -R vikunja:vikunja ./
|
||||
exec su vikunja -c /app/vikunja/vikunja "$@"
|
||||
else
|
||||
echo "info: creation of non-root user is skipped"
|
||||
exec /app/vikunja/vikunja "$@"
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
date: "2022-09-21:00:00+02:00"
|
||||
title: "Development"
|
||||
toc: true
|
||||
draft: false
|
||||
@@ -36,13 +36,13 @@ Make sure to check the other doc articles for specific development tasks like [t
|
||||
## Frontend requirements
|
||||
|
||||
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
|
||||
More instructions can be found in the repo's README.
|
||||
|
||||
You need to have yarn v1 and nodejs in version 16 installed.
|
||||
You need to have [pnpm](https://pnpm.io/) and nodejs in version 16 or 18 installed.
|
||||
|
||||
## Git flow
|
||||
|
||||
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically
|
||||
created from this branch.
|
||||
The `main` branch is the latest and bleeding edge branch with all changes. Unstable releases are automatically created from this branch.
|
||||
|
||||
A release gets tagged from the main branch with the version name as tag name.
|
||||
|
||||
@@ -50,7 +50,6 @@ Backports and point-releases should go to a `release/version` branch, based on t
|
||||
|
||||
## Conventional commits
|
||||
|
||||
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify
|
||||
generating release notes.
|
||||
We're using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) because they greatly simplify generating release notes.
|
||||
|
||||
It is not required to use them when creating a PR, but appreciated.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "Releasing a new Vikunja version"
|
||||
date: 2022-10-28T13:06:05+02:00
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Releasing a new Vikunja version
|
||||
|
||||
This checklist is a collection of all steps usually involved when releasing a new version of Vikunja.
|
||||
Not all steps are necessary for every release.
|
||||
|
||||
* Website update :
|
||||
* New Features: If there are new features worth mentioning the feature page should be updated.
|
||||
* New Screenshots: If an overhaul of an existing feature happend so that it now looks different from the existing screenshot, a new one is required.
|
||||
* Generate changelogs: (with git-cliff)
|
||||
* Frontend
|
||||
* API
|
||||
* Desktop
|
||||
* Tag a new version: Include the changelog for that version as the tag message
|
||||
* Frontend
|
||||
* API
|
||||
* Desktop
|
||||
* Once built: Prune the cloudflare cache so that the new versions show up at dl.vikunja.io
|
||||
* Release Highlights Blogpost:
|
||||
* Include a section about Vikunja in general (totally fine to copy one from the earlier blog posts)
|
||||
* New Features & Improvements: Mention bigger features, potentially with screenshots. Things like refactoring are sometimes also worth mentioneing.
|
||||
* Publish:
|
||||
* Reddit
|
||||
* Twitter
|
||||
* Mastodon
|
||||
* Chat
|
||||
* Newsletter
|
||||
* Forum
|
||||
* If features in the release were sponsored, send an email to relevant stakeholders
|
||||
* Update Vikunja Cloud version and other instances
|
||||
|
||||
@@ -98,12 +98,12 @@ Check out the docs [in the frontend repo](https://kolaente.dev/vikunja/frontend/
|
||||
To run the frontend unit tests, run
|
||||
|
||||
{{< highlight bash >}}
|
||||
yarn test:unit
|
||||
pnpm run test:unit
|
||||
{{< /highlight >}}
|
||||
|
||||
The frontend also has a watcher available that re-runs all unit tests every time you change something.
|
||||
To use it, simply run
|
||||
|
||||
{{< highlight bash >}}
|
||||
yarn test:unit-watch
|
||||
pnpm run test:unit-watch
|
||||
{{< /highlight >}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
date: "2022-09-21:00:00+02:00"
|
||||
title: "Build from sources"
|
||||
draft: false
|
||||
type: "doc"
|
||||
@@ -16,13 +16,13 @@ To completely build Vikunja from source, you need to build the api and frontend.
|
||||
|
||||
## API
|
||||
|
||||
The Vikunja API has no other dependencies than go itself.
|
||||
The Vikunja API has no other dependencies than go itself.
|
||||
That means compiling it boils down to these steps:
|
||||
|
||||
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.17`.
|
||||
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
|
||||
3. Clone the repo with `git clone https://code.vikunja.io/api` and switch into the directory.
|
||||
3. Run `mage build:build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
|
||||
4. Run `mage build:build` in the source of this repo. This will build a binary in the root of the repo which will be able to run on your system.
|
||||
|
||||
*Note:* Static ressources such as email templates are built into the binary.
|
||||
For these to work, you may need to run `mage build:generate` before building the vikunja binary.
|
||||
@@ -38,9 +38,7 @@ More options are available, please refer to the [magefile docs]({{< ref "../deve
|
||||
|
||||
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
|
||||
|
||||
You need to have yarn v1 and nodejs in version 16 installed.
|
||||
|
||||
1. Make sure [yarn v1](https://yarnpkg.com/getting-started/install) is properly installed on your system.
|
||||
3. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
|
||||
3. Install all dependencies with `yarn install`
|
||||
4. Build the frontend with `yarn build`. This will result in a js bundle in the `dist/` folder which you can deploy.
|
||||
1. Make sure you have [pnpm](https://pnpm.io/installation) properly installed on your system.
|
||||
2. Clone the repo with `git clone https://code.vikunja.io/frontend` and switch into the directory.
|
||||
3. Install all dependencies with `pnpm install`
|
||||
4. Build the frontend with `pnpm run build`. This will result in a static js bundle in the `dist/` folder which you can deploy.
|
||||
|
||||
@@ -657,7 +657,8 @@ Environment path: `VIKUNJA_MAILER_HOST`
|
||||
|
||||
### port
|
||||
|
||||
SMTP Host port
|
||||
SMTP Host port.
|
||||
**NOTE:** If you're unable to send mail and the only error you see in the logs is an `EOF`, try setting the port to `25`.
|
||||
|
||||
Default: `587`
|
||||
|
||||
@@ -839,7 +840,7 @@ Environment path: `VIKUNJA_LOG_HTTP`
|
||||
|
||||
### echo
|
||||
|
||||
Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
|
||||
Echo has its own logging which usually is unnecessary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
|
||||
|
||||
Default: `off`
|
||||
|
||||
@@ -852,7 +853,7 @@ Environment path: `VIKUNJA_LOG_ECHO`
|
||||
|
||||
Whether or not to log events. Useful for debugging. Possible values are stdout, stderr, file or off to disable events logging.
|
||||
|
||||
Default: `stdout`
|
||||
Default: `off`
|
||||
|
||||
Full path: `log.events`
|
||||
|
||||
@@ -968,17 +969,6 @@ Environment path: `VIKUNJA_FILES_MAXSIZE`
|
||||
|
||||
|
||||
|
||||
### wunderlist
|
||||
|
||||
These are the settings for the wunderlist migrator
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `migration.wunderlist`
|
||||
|
||||
Environment path: `VIKUNJA_MIGRATION_WUNDERLIST`
|
||||
|
||||
|
||||
### todoist
|
||||
|
||||
Default: `<empty>`
|
||||
@@ -1173,3 +1163,132 @@ Full path: `metrics.password`
|
||||
Environment path: `VIKUNJA_METRICS_PASSWORD`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## defaultsettings
|
||||
|
||||
Provide default settings for new users. When a new user is created, these settings will automatically be set for the user. If you change them in the config file afterwards they will not be changed back for existing users.
|
||||
|
||||
|
||||
|
||||
### avatar_provider
|
||||
|
||||
The avatar source for the user. Can be `gravatar`, `initials`, `upload` or `marble`. If you set this to `upload` you'll also need to specify `defaultsettings.avatar_file_id`.
|
||||
|
||||
Default: `initials`
|
||||
|
||||
Full path: `defaultsettings.avatar_provider`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_AVATAR_PROVIDER`
|
||||
|
||||
|
||||
### avatar_file_id
|
||||
|
||||
The id of the file used as avatar.
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `defaultsettings.avatar_file_id`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_AVATAR_FILE_ID`
|
||||
|
||||
|
||||
### email_reminders_enabled
|
||||
|
||||
If set to true users will get task reminders via email.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `defaultsettings.email_reminders_enabled`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_EMAIL_REMINDERS_ENABLED`
|
||||
|
||||
|
||||
### discoverable_by_name
|
||||
|
||||
If set to true will allow other users to find this user when searching for parts of their name.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `defaultsettings.discoverable_by_name`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_NAME`
|
||||
|
||||
|
||||
### discoverable_by_email
|
||||
|
||||
If set to true will allow other users to find this user when searching for their exact email.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `defaultsettings.discoverable_by_email`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_DISCOVERABLE_BY_EMAIL`
|
||||
|
||||
|
||||
### overdue_tasks_reminders_enabled
|
||||
|
||||
If set to true will send an email every day with all overdue tasks at a configured time.
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `defaultsettings.overdue_tasks_reminders_enabled`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_ENABLED`
|
||||
|
||||
|
||||
### overdue_tasks_reminders_time
|
||||
|
||||
When to send the overdue task reminder email.
|
||||
|
||||
Default: `9:00`
|
||||
|
||||
Full path: `defaultsettings.overdue_tasks_reminders_time`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_OVERDUE_TASKS_REMINDERS_TIME`
|
||||
|
||||
|
||||
### default_list_id
|
||||
|
||||
The id of the default list. Make sure users actually have access to this list when setting this value.
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `defaultsettings.default_list_id`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_DEFAULT_LIST_ID`
|
||||
|
||||
|
||||
### week_start
|
||||
|
||||
Start of the week for the user. `0` is sunday, `1` is monday and so on.
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `defaultsettings.week_start`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_WEEK_START`
|
||||
|
||||
|
||||
### language
|
||||
|
||||
The language of the user interface. Must be an ISO 639-1 language code. Will default to the browser language the user uses when signing up.
|
||||
|
||||
Default: `<unset>`
|
||||
|
||||
Full path: `defaultsettings.language`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_LANGUAGE`
|
||||
|
||||
|
||||
### timezone
|
||||
|
||||
The time zone of each individual user. This will affect when users get reminders and overdue task emails.
|
||||
|
||||
Default: `<time zone set at service.timezone>`
|
||||
|
||||
Full path: `defaultsettings.timezone`
|
||||
|
||||
Environment path: `VIKUNJA_DEFAULTSETTINGS_TIMEZONE`
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ menu:
|
||||
|
||||
# Full docker example
|
||||
|
||||
This docker compose configuration will run Vikunja with backend and frontend with a mariadb as database.
|
||||
This docker compose configuration will run Vikunja with backend and frontend with a mariadb database.
|
||||
It uses an nginx container or traefik on the host to proxy backend and frontend into a single port.
|
||||
|
||||
For all available configuration options, see [configuration]({{< ref "config.md">}}).
|
||||
@@ -76,7 +76,7 @@ This example lets you host Vikunja without any reverse proxy in front of it. Thi
|
||||
you need to get something up and running. If you want to host Vikunja on one single port instead of two different ones
|
||||
or need tls termination, check out one of the other examples.
|
||||
|
||||
Not that you need to change the `VIKUNJA_API_URL` environment variable to the ip (the docker host you're running this on)
|
||||
Note that you need to change the `VIKUNJA_API_URL` environment variable to the ip (the docker host you're running this on)
|
||||
is reachable at. Because the browser you'll use to access the Vikunja frontend uses that url to make the requests, it
|
||||
has to be able to reach that ip + port from the outside. Putting everything in a private network won't work.
|
||||
|
||||
@@ -125,7 +125,7 @@ services:
|
||||
|
||||
This example assumes [traefik](https://traefik.io) version 2 installed and configured to [use docker as a configuration provider](https://docs.traefik.io/providers/docker/).
|
||||
|
||||
We also make a few assumtions here which you'll most likely need to adjust for your traefik setup:
|
||||
We also make a few assumptions here which you'll most likely need to adjust for your traefik setup:
|
||||
|
||||
* Your domain is `vikunja.example.com`
|
||||
* The entrypoint you want to make vikunja available from is called `https`
|
||||
@@ -155,7 +155,7 @@ services:
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.vikunja-api.rule=Host(`vikunja.example.com`) && PathPrefix(`/api/v1`, `/dav/`, `/.well-known/`)"
|
||||
- "traefik.http.routers.vikunja-api.rule=Host(`vikunja.example.com`) && (PathPrefix(`/api/v1`) || PathPrefix(`/dav/`) || PathPrefix(`/.well-known/`))"
|
||||
- "traefik.http.routers.vikunja-api.entrypoints=https"
|
||||
- "traefik.http.routers.vikunja-api.tls.certResolver=acme"
|
||||
frontend:
|
||||
@@ -398,7 +398,7 @@ docker main folders:
|
||||
* vikunja
|
||||
* mariadb
|
||||
|
||||
Synology has it's own GUI for managing Docker containers... But it's easier via docker compose.
|
||||
Synology has its own GUI for managing Docker containers... But it's easier via docker compose.
|
||||
|
||||
To do that, you can
|
||||
|
||||
@@ -407,7 +407,7 @@ To do that, you can
|
||||
* without activating SSH, by using Portainer (you have to install first, check out [this tutorial](https://www.portainer.io/blog/how-to-install-portainer-on-a-synology-nas) for exmple):
|
||||
1. Go to **Dashboard / Stacks** click the button **"Add Stack"**
|
||||
2. Give it the name Vikunja and paste the adapted docker compose file
|
||||
3. Deploy the Stack with the "Delpoy Stack" button:
|
||||
3. Deploy the Stack with the "Deploy Stack" button:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ WantedBy=multi-user.target
|
||||
|
||||
If you've installed Vikunja to a directory other than `/opt/vikunja`, you need to adapt `WorkingDirectory` accordingly.
|
||||
|
||||
After you made all nessecary modifications, it's time to start the service:
|
||||
After you made all necessary modifications, it's time to start the service:
|
||||
|
||||
{{< highlight bash >}}
|
||||
sudo systemctl enable vikunja
|
||||
@@ -97,7 +97,7 @@ To build vikunja from source, see [building from source]({{< ref "build-from-sou
|
||||
### Updating
|
||||
|
||||
Simply replace the binary and templates with the new version, then restart Vikunja.
|
||||
It will automatically run all nessecary database migrations.
|
||||
It will automatically run all necessary database migrations.
|
||||
**Make sure to take a look at the changelog for the new version to not miss any manual steps the update may involve!**
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -35,7 +35,7 @@ Just open the file with a text editor - there are comments which will explain ho
|
||||
|
||||
## Docker
|
||||
|
||||
The docker image is based on nginx and just contains all nessecary files for the frontend.
|
||||
The docker image is based on nginx and just contains all necessary files for the frontend.
|
||||
|
||||
To run it, all you need is
|
||||
|
||||
@@ -47,10 +47,7 @@ which will run the docker image and expose port 80 on the host.
|
||||
|
||||
See [full docker example]({{< ref "full-docker-example.md">}}) for more varations of this config.
|
||||
|
||||
### Setting user and group id of the user running vikunja
|
||||
|
||||
You can set the user and group id of the user running vikunja with the `PUID` and `PGID` evironment variables.
|
||||
This follows the pattern used by [the linuxserver.io](https://docs.linuxserver.io/general/understanding-puid-and-pgid) docker images.
|
||||
The docker container runs as an unprivileged user and does not mount anything.
|
||||
|
||||
### API URL configuration in docker
|
||||
|
||||
|
||||
@@ -10,6 +10,6 @@ menu:
|
||||
|
||||
There are two third-party Helm-Charts which can be used to host Vikunja with k8s:
|
||||
|
||||
* [Truecharts](https://truecharts.org/docs/charts/stable/vikunja/)
|
||||
* [Truecharts](https://truecharts.org/charts/stable/vikunja/)
|
||||
* [k8s at Home](https://github.com/k8s-at-home/charts)
|
||||
|
||||
|
||||
@@ -37,9 +37,32 @@ Authelia config:
|
||||
description: Vikunja
|
||||
secret: <vikunja secret>
|
||||
redirect_uris:
|
||||
- https://vikunja.mydomain.com/auth/openid/ <----- Matching slash at the end
|
||||
- https://vikunja.mydomain.com/auth/openid/authelia
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
```
|
||||
|
||||
## Google / Google Workspace
|
||||
|
||||
Vikunja Config:
|
||||
|
||||
```yaml
|
||||
openid:
|
||||
enabled: true
|
||||
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
|
||||
providers:
|
||||
- name: Google
|
||||
authurl: https://accounts.google.com
|
||||
clientid: <google-oauth-client-id>
|
||||
clientsecret: <google-oauth-client-secret>
|
||||
```
|
||||
|
||||
Google config:
|
||||
|
||||
- Navigate to https://console.cloud.google.com/apis/credentials in the target project
|
||||
- Create a new OAuth client ID
|
||||
- Configure an authorized redirect URI of https://vikunja.mydomain.com/auth/openid/google
|
||||
|
||||
Note that there currently seems to be no way to stop creation of new users, even when enableregistration is false in the configuration. This means that this approach works well only with an "Internal Organization" app for Google Workspace, which limits the allowed users to organizational accounts only. External / public applications will potentially allow every Google user to register.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: "Running Vikunja in a subdirectory"
|
||||
date: 2022-09-23T12:15:04+02:00
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Running Vikunja in a subdirectory
|
||||
|
||||
Running Vikunja in a subdirectory is not supported out of the box.
|
||||
However, you can still run it in a subdirectory but need to build the frontend yourself.
|
||||
|
||||
## Frontend
|
||||
|
||||
First, make sure you're able to build the frontend from source.
|
||||
Check [the guide about building from source]({{< ref "build-from-source.md">}}#frontend) about that.
|
||||
|
||||
### Dynamicly set with build command
|
||||
|
||||
Run the build with the `VIKUNJA_FRONTEND_BASE` variable specified.
|
||||
|
||||
```
|
||||
VIKUNJA_FRONTEND_BASE=/SUBPATH/ pnpm run build
|
||||
```
|
||||
|
||||
Where `SUBPATH` is the subdirectory you want to run Vikunja on.
|
||||
|
||||
### Set via .env.local
|
||||
|
||||
* Copy `.env.local.example` to `.env.local`
|
||||
* Uncomment `VIKUNJA_FRONTEND_BASE` and set `/subpath/` to the desired path.
|
||||
|
||||
After saving, build Vikunja as normal.
|
||||
|
||||
```
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Once you have the build files you can deploy them as usual.
|
||||
Note that when deploying in docker you'll need to put the files in a web container yourself, you
|
||||
can't use the `Dockerfile` in the repo without modifications.
|
||||
|
||||
## API
|
||||
|
||||
If you're not using a reverse proxy you're good to go.
|
||||
Simply configure the api url in the frontend as you normally would.
|
||||
|
||||
If you're using a reverse proxy you'll need to adjust the paths so that the api is available at `/SUBPATH/api/v1`.
|
||||
You can check if everything is working correctly by opening `/SUBPATH/api/v1/info` in a browser.
|
||||
@@ -10,9 +10,9 @@ menu:
|
||||
|
||||
# Caldav
|
||||
|
||||
> **Warning:** The caldav integration is in an early alpha stage and has bugs.
|
||||
> **Warning:** The caldav integration is in an early alpha stage and has bugs.
|
||||
> It works well with some clients while having issues with others.
|
||||
> If you encounter issues, please [report them](https://code.vikunja.io/api/issues/new?body=[caldav])
|
||||
> If you encounter issues, please [report them](https://code.vikunja.io/api/issues/new?body=[caldav])
|
||||
|
||||
Vikunja supports managing tasks via the [caldav VTODO](https://tools.ietf.org/html/rfc5545#section-3.6.2) extension.
|
||||
|
||||
@@ -37,6 +37,7 @@ Vikunja currently supports the following properties:
|
||||
* `SUMMARY`
|
||||
* `DESCRIPTION`
|
||||
* `PRIORITY`
|
||||
* `CATEGORIES`
|
||||
* `COMPLETED`
|
||||
* `DUE`
|
||||
* `DTSTART`
|
||||
@@ -46,11 +47,11 @@ Vikunja currently supports the following properties:
|
||||
* `CREATED`
|
||||
* `DTSTAMP`
|
||||
* `LAST-MODIFIED`
|
||||
* Recurrence
|
||||
|
||||
Vikunja **currently does not** support these properties:
|
||||
|
||||
* `ATTACH`
|
||||
* `CATEGORIES`
|
||||
* `CLASS`
|
||||
* `COMMENT`
|
||||
* `GEO`
|
||||
@@ -61,7 +62,6 @@ Vikunja **currently does not** support these properties:
|
||||
* `CONTACT`
|
||||
* `RECURRENCE-ID`
|
||||
* `URL`
|
||||
* Recurrence
|
||||
* `SEQUENCE`
|
||||
|
||||
## Tested Clients
|
||||
|
||||
@@ -26,6 +26,15 @@ If you don't specify a command, the [`web`](#web) command will be executed.
|
||||
|
||||
All commands use the same standard [config file]({{< ref "../setup/config.md">}}).
|
||||
|
||||
## Using the cli in docker
|
||||
|
||||
When running Vikunja in docker, you'll need to execute all commands in the `api` container.
|
||||
Instead of running the `vikunja` binary directly, run it like this:
|
||||
|
||||
```
|
||||
docker exec <name of the vikunja api container> /app/vikunja/vikunja <subcommand>
|
||||
```
|
||||
|
||||
### `dump`
|
||||
|
||||
Creates a zip file with all vikunja-related files.
|
||||
@@ -127,6 +136,21 @@ Flags:
|
||||
* `-p`, `--password`: The password of the new user. You will be asked to enter it if not provided through the flag.
|
||||
* `-u`, `--username`: The username of the new user.
|
||||
|
||||
#### `user delete`
|
||||
|
||||
Start the user deletion process.
|
||||
If called without the `--now` flag, this command will only trigger an email to the user in order for them to confirm and start the deletion process (this is the same behavoir as if the user requested their deletion via the web interface).
|
||||
With the flag the user is deleted **immediately**.
|
||||
|
||||
**USE WITH CAUTION.**
|
||||
|
||||
{{< highlight bash >}}
|
||||
$ vikunja user delete <id> <flags>
|
||||
{{< /highlight >}}
|
||||
|
||||
Flags:
|
||||
* `-n`, `--now` If provided, deletes the user immediately instead of emailing them first.
|
||||
|
||||
#### `user list`
|
||||
|
||||
Shows a list of all users.
|
||||
|
||||
@@ -54,13 +54,14 @@ This document describes the different errors Vikunja can return.
|
||||
|
||||
| ErrorCode | HTTP Status Code | Description |
|
||||
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 3001 | 404 | The list does not exist. |
|
||||
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
|
||||
| 3005 | 400 | The list title cannot be empty. |
|
||||
| 3006 | 404 | The list share does not exist. |
|
||||
| 3007 | 400 | A list with this identifier already exists. |
|
||||
| 3008 | 412 | The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list. |
|
||||
| 3009 | 412 | The list cannot belong to a dynamically generated namespace like "Favorites". |
|
||||
| 3001 | 404 | The list does not exist. |
|
||||
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
|
||||
| 3005 | 400 | The list title cannot be empty. |
|
||||
| 3006 | 404 | The list share does not exist. |
|
||||
| 3007 | 400 | A list with this identifier already exists. |
|
||||
| 3008 | 412 | The list is archived and can therefore only be accessed read only. This is also true for all tasks associated with this list. |
|
||||
| 3009 | 412 | The list cannot belong to a dynamically generated namespace like "Favorites". |
|
||||
| 3010 | 412 | The list must belong to a namespace. |
|
||||
|
||||
## Task
|
||||
|
||||
@@ -81,10 +82,12 @@ This document describes the different errors Vikunja can return.
|
||||
| 4013 | 400 | The task sort param is invalid. |
|
||||
| 4014 | 400 | The task sort order is invalid. |
|
||||
| 4015 | 404 | The task comment does not exist. |
|
||||
| 4016 | 403 | Invalid task field. |
|
||||
| 4017 | 403 | Invalid task filter comparator. |
|
||||
| 4018 | 403 | Invalid task filter concatinator. |
|
||||
| 4019 | 403 | Invalid task filter value. |
|
||||
| 4016 | 400 | Invalid task field. |
|
||||
| 4017 | 400 | Invalid task filter comparator. |
|
||||
| 4018 | 400 | Invalid task filter concatinator. |
|
||||
| 4019 | 400 | Invalid task filter value. |
|
||||
| 4020 | 400 | The provided attachment does not belong to that task. |
|
||||
| 4021 | 400 | This user is already assigned to that task. |
|
||||
|
||||
## Namespace
|
||||
|
||||
|
||||
@@ -19,62 +19,63 @@ module code.vikunja.io/api
|
||||
require (
|
||||
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
|
||||
gitea.com/xorm/xorm-redis-cache v0.2.0
|
||||
github.com/ThreeDotsLabs/watermill v1.1.1
|
||||
github.com/ThreeDotsLabs/watermill v1.2.0
|
||||
github.com/adlio/trello v1.10.0
|
||||
github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182
|
||||
github.com/arran4/golang-ical v0.0.0-20230213232137-07c6aad5e4f0
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||
github.com/coreos/go-oidc/v3 v3.2.0
|
||||
github.com/coreos/go-oidc/v3 v3.5.0
|
||||
github.com/cweill/gotests v1.6.0
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/getsentry/sentry-go v0.13.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/getsentry/sentry-go v0.19.0
|
||||
github.com/go-sql-driver/mysql v1.7.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
github.com/gocarina/gocsv v0.0.0-20230226133904-70c27cb2918a
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/imdario/mergo v0.3.13
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/labstack/echo/v4 v4.8.0
|
||||
github.com/labstack/gommon v0.3.1
|
||||
github.com/lib/pq v1.10.6
|
||||
github.com/magefile/mage v1.13.0
|
||||
github.com/mattn/go-sqlite3 v1.14.15
|
||||
github.com/labstack/echo-jwt/v4 v4.1.0
|
||||
github.com/labstack/echo/v4 v4.10.2
|
||||
github.com/labstack/gommon v0.4.0
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/magefile/mage v1.14.0
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.14.0
|
||||
github.com/redis/go-redis/v9 v9.0.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/swaggo/swag v1.8.4
|
||||
github.com/spf13/afero v1.9.5
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.15.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/swaggo/swag v1.8.10
|
||||
github.com/tkuchiki/go-timezone v0.2.2
|
||||
github.com/ulule/limiter/v3 v3.10.0
|
||||
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93
|
||||
github.com/wneessen/go-mail v0.2.6
|
||||
github.com/yuin/goldmark v1.4.13
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
|
||||
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539
|
||||
golang.org/x/oauth2 v0.0.0-20220808172628-8227340efae7
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab
|
||||
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035
|
||||
github.com/ulule/limiter/v3 v3.11.1
|
||||
github.com/vectordotdev/go-datemath v0.1.1-0.20220323213446-f3954d0b18ae
|
||||
github.com/wneessen/go-mail v0.3.8
|
||||
github.com/yuin/goldmark v1.5.4
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/image v0.6.0
|
||||
golang.org/x/oauth2 v0.6.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.6.0
|
||||
golang.org/x/term v0.6.0
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
src.techknowlogick.com/xgo v1.4.1-0.20210311222705-d25c33fcd864
|
||||
src.techknowlogick.com/xormigrate v1.4.0
|
||||
xorm.io/builder v0.3.9
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.1.2
|
||||
src.techknowlogick.com/xgo v1.7.1-0.20230307171022-b60708668fc7
|
||||
src.techknowlogick.com/xormigrate v1.5.0
|
||||
xorm.io/builder v0.3.12
|
||||
xorm.io/xorm v1.3.2
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -84,76 +85,70 @@ require (
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/garyburd/redigo v1.6.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-chi/chi v4.0.2+incompatible // indirect
|
||||
github.com/go-errors/errors v1.1.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.8 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.4 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.16.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.39.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.3.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.3.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/coreos/bbolt => go.etcd.io/bbolt v1.3.4
|
||||
github.com/coreos/go-systemd => github.com/coreos/go-systemd/v22 v22.0.0
|
||||
github.com/hpcloud/tail => github.com/jeffbean/tail v1.0.1 // See https://github.com/hpcloud/tail/pull/159
|
||||
github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
|
||||
gopkg.in/fsnotify.v1 => github.com/kolaente/fsnotify v1.4.10-0.20200411160148-1bc3c8ff4048 // See https://github.com/fsnotify/fsnotify/issues/328 and https://github.com/golang/go/issues/26904
|
||||
)
|
||||
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
+27
-12
@@ -27,7 +27,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -406,7 +405,7 @@ func checkGolangCiLintInstalled() {
|
||||
mg.Deps(initVars)
|
||||
if err := exec.Command("golangci-lint").Run(); err != nil && strings.Contains(err.Error(), "executable file not found") {
|
||||
fmt.Println("Please manually install golangci-lint by running")
|
||||
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.47.3")
|
||||
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.51.2")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -547,6 +546,20 @@ func (Release) Darwin() error {
|
||||
return runXgo("darwin-10.15/*")
|
||||
}
|
||||
|
||||
func (Release) Xgo(target string) error {
|
||||
parts := strings.Split(target, "/")
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid target")
|
||||
}
|
||||
|
||||
variant := ""
|
||||
if len(parts) > 2 && parts[2] != "" {
|
||||
variant = "-" + strings.ReplaceAll(parts[2], "v", "")
|
||||
}
|
||||
|
||||
return runXgo(parts[0] + "/" + parts[1] + variant)
|
||||
}
|
||||
|
||||
// Compresses the built binaries in dist/binaries/ to reduce their filesize
|
||||
func (Release) Compress(ctx context.Context) error {
|
||||
// $(foreach file,$(filter-out $(wildcard $(wildcard $(DIST)/binaries/$(EXECUTABLE)-*mips*)),$(wildcard $(DIST)/binaries/$(EXECUTABLE)-*)), upx -9 $(file);)
|
||||
@@ -559,7 +572,9 @@ func (Release) Compress(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
// No mips or s390x for you today
|
||||
if strings.Contains(info.Name(), "mips") || strings.Contains(info.Name(), "s390x") {
|
||||
if strings.Contains(info.Name(), "mips") ||
|
||||
strings.Contains(info.Name(), "s390x") ||
|
||||
strings.Contains(info.Name(), "riscv64") { // not supported by upx
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -663,7 +678,7 @@ func (Release) Zip() error {
|
||||
|
||||
fmt.Printf("Zipping %s...\n", info.Name())
|
||||
|
||||
c := exec.Command("zip", "-r", RootPath+"/"+DIST+"/zip/"+info.Name(), ".", "-i", "*")
|
||||
c := exec.Command("zip", "-r", RootPath+"/"+DIST+"/zip/"+info.Name()+".zip", ".", "-i", "*")
|
||||
c.Dir = path
|
||||
out, err := c.Output()
|
||||
fmt.Print(string(out))
|
||||
@@ -688,7 +703,7 @@ func (Release) Packages() error {
|
||||
binpath := "nfpm"
|
||||
err = exec.Command(binpath).Run()
|
||||
if err != nil && strings.Contains(err.Error(), "executable file not found") {
|
||||
binpath = "/nfpm"
|
||||
binpath = "/usr/bin/nfpm"
|
||||
err = exec.Command(binpath).Run()
|
||||
}
|
||||
if err != nil && strings.Contains(err.Error(), "executable file not found") {
|
||||
@@ -697,16 +712,16 @@ func (Release) Packages() error {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Because nfpm does not support templating, we replace the values in the config file and restore it after running
|
||||
// Because nfpm does not support templating, we replace the values in the config file and restore it after running
|
||||
nfpmConfigPath := RootPath + "/nfpm.yaml"
|
||||
nfpmconfig, err := ioutil.ReadFile(nfpmConfigPath)
|
||||
nfpmconfig, err := os.ReadFile(nfpmConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fixedConfig := strings.ReplaceAll(string(nfpmconfig), "<version>", VersionNumber)
|
||||
fixedConfig = strings.ReplaceAll(fixedConfig, "<binlocation>", BinLocation)
|
||||
if err := ioutil.WriteFile(nfpmConfigPath, []byte(fixedConfig), 0); err != nil {
|
||||
if err := os.WriteFile(nfpmConfigPath, []byte(fixedConfig), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -719,7 +734,7 @@ func (Release) Packages() error {
|
||||
runAndStreamOutput(binpath, "pkg", "--packager", "rpm", "--target", releasePath)
|
||||
runAndStreamOutput(binpath, "pkg", "--packager", "apk", "--target", releasePath)
|
||||
|
||||
return ioutil.WriteFile(nfpmConfigPath, nfpmconfig, 0)
|
||||
return os.WriteFile(nfpmConfigPath, nfpmconfig, 0)
|
||||
}
|
||||
|
||||
type Dev mg.Namespace
|
||||
@@ -883,7 +898,7 @@ func (s *` + name + `) Handle(msg *message.Message) (err error) {
|
||||
if _, err := f.Seek(idx, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
remainder, err := ioutil.ReadAll(f)
|
||||
remainder, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1072,7 +1087,7 @@ const (
|
||||
// Generates the config docs from a commented config.yml.sample file in the repo root.
|
||||
func GenerateDocs() error {
|
||||
|
||||
config, err := ioutil.ReadFile("config.yml.sample")
|
||||
config, err := os.ReadFile("config.yml.sample")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1122,7 +1137,7 @@ func GenerateDocs() error {
|
||||
|
||||
// We write the full file to prevent old content leftovers at the end
|
||||
// I know, there are probably better ways to do this.
|
||||
if err := ioutil.WriteFile(configDocPath, []byte(fullConfig), 0); err != nil {
|
||||
if err := os.WriteFile(configDocPath, []byte(fullConfig), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+29
-12
@@ -22,7 +22,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
)
|
||||
@@ -57,11 +58,13 @@ type Todo struct {
|
||||
Priority int64 // 0-9, 1 is highest
|
||||
RelatedToUID string
|
||||
Color string
|
||||
|
||||
Start time.Time
|
||||
End time.Time
|
||||
DueDate time.Time
|
||||
Duration time.Duration
|
||||
Categories []string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
DueDate time.Time
|
||||
Duration time.Duration
|
||||
RepeatAfter int64
|
||||
RepeatMode models.TaskRepeatMode
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time // last-mod
|
||||
@@ -181,6 +184,10 @@ SUMMARY:` + t.Summary + getCaldavColor(t.Color)
|
||||
if t.Start.Unix() > 0 {
|
||||
caldavtodos += `
|
||||
DTSTART:` + makeCalDavTimeFromTimeStamp(t.Start)
|
||||
if t.Duration != 0 && t.DueDate.Unix() == 0 {
|
||||
caldavtodos += `
|
||||
DURATION:PT` + formatDuration(t.Duration)
|
||||
}
|
||||
}
|
||||
if t.End.Unix() > 0 {
|
||||
caldavtodos += `
|
||||
@@ -217,16 +224,26 @@ DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
|
||||
CREATED:` + makeCalDavTimeFromTimeStamp(t.Created)
|
||||
}
|
||||
|
||||
if t.Duration != 0 {
|
||||
caldavtodos += `
|
||||
DURATION:PT` + formatDuration(t.Duration)
|
||||
}
|
||||
|
||||
if t.Priority != 0 {
|
||||
caldavtodos += `
|
||||
PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority))
|
||||
}
|
||||
|
||||
if t.RepeatAfter > 0 || t.RepeatMode == models.TaskRepeatModeMonth {
|
||||
if t.RepeatMode == models.TaskRepeatModeMonth {
|
||||
caldavtodos += `
|
||||
RRULE:FREQ=MONTHLY;BYMONTHDAY=` + t.DueDate.Format("02") // Day of the month
|
||||
} else {
|
||||
caldavtodos += `
|
||||
RRULE:FREQ=SECONDLY;INTERVAL=` + strconv.FormatInt(t.RepeatAfter, 10)
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.Categories) > 0 {
|
||||
caldavtodos += `
|
||||
CATEGORIES:` + strings.Join(t.Categories, ",")
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
|
||||
|
||||
@@ -241,7 +258,7 @@ END:VCALENDAR` // Need a line break
|
||||
}
|
||||
|
||||
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
|
||||
return ts.In(config.GetTimeZone()).Format(DateFormat)
|
||||
return ts.In(time.UTC).Format(DateFormat) + "Z"
|
||||
}
|
||||
|
||||
func calcAlarmDateFromReminder(eventStart, reminder time.Time) (alarmTime string) {
|
||||
|
||||
+145
-33
@@ -20,6 +20,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -84,25 +86,25 @@ X-APPLE-CALENDAR-COLOR:#affffeFF
|
||||
X-OUTLOOK-COLOR:#affffeFF
|
||||
X-FUNAMBOL-COLOR:#affffeFF
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
DTSTAMP:20181201T011204
|
||||
DTSTART:20181201T011204
|
||||
DTEND:20181201T013024
|
||||
DTSTAMP:20181201T011204Z
|
||||
DTSTART:20181201T011204Z
|
||||
DTEND:20181201T013024Z
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:randommduidd
|
||||
SUMMARY:Event #2
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T045844
|
||||
DTSTART:20181202T045844
|
||||
DTEND:20181202T081844
|
||||
DTSTAMP:20181202T045844Z
|
||||
DTSTART:20181202T045844Z
|
||||
DTEND:20181202T081844Z
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
|
||||
SUMMARY:Event #3 with empty uid
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T050024
|
||||
DTSTART:20181202T050024
|
||||
DTEND:20181202T050320
|
||||
DTSTAMP:20181202T050024Z
|
||||
DTSTART:20181202T050024Z
|
||||
DTEND:20181202T050320Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
@@ -169,9 +171,9 @@ BEGIN:VEVENT
|
||||
UID:randommduid
|
||||
SUMMARY:Event #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
DTSTAMP:20181201T011204
|
||||
DTSTART:20181201T011204
|
||||
DTEND:20181201T013024
|
||||
DTSTAMP:20181201T011204Z
|
||||
DTSTART:20181201T011204Z
|
||||
DTEND:20181201T013024Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT3M20S
|
||||
ACTION:DISPLAY
|
||||
@@ -192,9 +194,9 @@ BEGIN:VEVENT
|
||||
UID:randommduidd
|
||||
SUMMARY:Event #2
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T045844
|
||||
DTSTART:20181202T045844
|
||||
DTEND:20181202T081844
|
||||
DTSTAMP:20181202T045844Z
|
||||
DTSTART:20181202T045844Z
|
||||
DTEND:20181202T081844Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT27H50M0S
|
||||
ACTION:DISPLAY
|
||||
@@ -212,12 +214,12 @@ DESCRIPTION:Event #2
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20181202T0500242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
|
||||
UID:20181202T050024Z2aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
|
||||
SUMMARY:Event #3 with empty uid
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T050024
|
||||
DTSTART:20181202T050024
|
||||
DTEND:20181202T050320
|
||||
DTSTAMP:20181202T050024Z
|
||||
DTSTART:20181202T050024Z
|
||||
DTEND:20181202T050320Z
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT27H51M40S
|
||||
ACTION:DISPLAY
|
||||
@@ -240,12 +242,12 @@ DESCRIPTION:Event #3 with empty uid
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:20181202T050024ae7548ce9556df85038abe90dc674d4741a61ce74d1cf
|
||||
UID:20181202T050024Zae7548ce9556df85038abe90dc674d4741a61ce74d1cf
|
||||
SUMMARY:Event #4 without any
|
||||
DESCRIPTION:
|
||||
DTSTAMP:20181202T050024
|
||||
DTSTART:20181202T050024
|
||||
DTEND:20181202T050320
|
||||
DTSTAMP:20181202T050024Z
|
||||
DTSTART:20181202T050024Z
|
||||
DTEND:20181202T050320Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
@@ -278,9 +280,9 @@ BEGIN:VEVENT
|
||||
UID:randommduid
|
||||
SUMMARY:Event #1
|
||||
DESCRIPTION:Lorem Ipsum\nDolor sit amet
|
||||
DTSTAMP:20181201T011204
|
||||
DTSTART:20181201T011204
|
||||
DTEND:20181201T013024
|
||||
DTSTAMP:20181201T011204Z
|
||||
DTSTART:20181201T011204Z
|
||||
DTEND:20181201T013024Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
@@ -333,13 +335,13 @@ X-OUTLOOK-COLOR:#ffffffFF
|
||||
X-FUNAMBOL-COLOR:#ffffffFF
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Todo #1
|
||||
X-APPLE-CALENDAR-COLOR:#affffeFF
|
||||
X-OUTLOOK-COLOR:#affffeFF
|
||||
X-FUNAMBOL-COLOR:#affffeFF
|
||||
DESCRIPTION:Lorem Ipsum\nDolor sit amet
|
||||
LAST-MODIFIED:00010101T000000
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
@@ -368,12 +370,12 @@ X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
COMPLETED:20181201T013024
|
||||
COMPLETED:20181201T013024Z
|
||||
STATUS:COMPLETED
|
||||
LAST-MODIFIED:00010101T000000
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
@@ -402,11 +404,121 @@ X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
PRIORITY:9
|
||||
LAST-MODIFIED:00010101T000000
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "with repeating monthly",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
},
|
||||
todos: []*Todo{
|
||||
{
|
||||
Summary: "Todo #1",
|
||||
Description: "Lorem Ipsum",
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
RepeatMode: models.TaskRepeatModeMonth,
|
||||
DueDate: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavtasks: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
DUE:20181201T011204Z
|
||||
RRULE:FREQ=MONTHLY;BYMONTHDAY=01
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "with repeat mode default",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
},
|
||||
todos: []*Todo{
|
||||
{
|
||||
Summary: "Todo #1",
|
||||
Description: "Lorem Ipsum",
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
RepeatMode: models.TaskRepeatModeDefault,
|
||||
DueDate: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
RepeatAfter: 435,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavtasks: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
DUE:20181201T011204Z
|
||||
RRULE:FREQ=SECONDLY;INTERVAL=435
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "with categories",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
Color: "ffffff",
|
||||
},
|
||||
todos: []*Todo{
|
||||
{
|
||||
Summary: "Todo #1",
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Color: "affffe",
|
||||
Categories: []string{"label1", "label2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavtasks: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
X-APPLE-CALENDAR-COLOR:#ffffffFF
|
||||
X-OUTLOOK-COLOR:#ffffffFF
|
||||
X-FUNAMBOL-COLOR:#ffffffFF
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Todo #1
|
||||
X-APPLE-CALENDAR-COLOR:#affffeFF
|
||||
X-OUTLOOK-COLOR:#affffeFF
|
||||
X-FUNAMBOL-COLOR:#affffeFF
|
||||
CATEGORIES:label1,label2
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
|
||||
+26
-7
@@ -34,6 +34,10 @@ func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*m
|
||||
for _, t := range listTasks {
|
||||
|
||||
duration := t.EndDate.Sub(t.StartDate)
|
||||
var categories []string
|
||||
for _, label := range t.Labels {
|
||||
categories = append(categories, label.Title)
|
||||
}
|
||||
|
||||
caldavtodos = append(caldavtodos, &Todo{
|
||||
Timestamp: t.Updated,
|
||||
@@ -42,13 +46,16 @@ func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*m
|
||||
Description: t.Description,
|
||||
Completed: t.DoneAt,
|
||||
// Organizer: &t.CreatedBy, // Disabled until we figure out how this works
|
||||
Priority: t.Priority,
|
||||
Start: t.StartDate,
|
||||
End: t.EndDate,
|
||||
Created: t.Created,
|
||||
Updated: t.Updated,
|
||||
DueDate: t.DueDate,
|
||||
Duration: duration,
|
||||
Categories: categories,
|
||||
Priority: t.Priority,
|
||||
Start: t.StartDate,
|
||||
End: t.EndDate,
|
||||
Created: t.Created,
|
||||
Updated: t.Updated,
|
||||
DueDate: t.DueDate,
|
||||
Duration: duration,
|
||||
RepeatAfter: t.RepeatAfter,
|
||||
RepeatMode: t.RepeatMode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,11 +96,23 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
||||
description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",")
|
||||
description = strings.ReplaceAll(description, "\\n", "\n")
|
||||
|
||||
var labels []*models.Label
|
||||
if val, ok := task["CATEGORIES"]; ok {
|
||||
categories := strings.Split(val, ",")
|
||||
labels = make([]*models.Label, 0, len(categories))
|
||||
for _, category := range categories {
|
||||
labels = append(labels, &models.Label{
|
||||
Title: category,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
vTask = &models.Task{
|
||||
UID: task["UID"],
|
||||
Title: task["SUMMARY"],
|
||||
Description: description,
|
||||
Priority: priority,
|
||||
Labels: labels,
|
||||
DueDate: caldavTimeToTimestamp(task["DUE"]),
|
||||
Updated: caldavTimeToTimestamp(task["DTSTAMP"]),
|
||||
StartDate: caldavTimeToTimestamp(task["DTSTART"]),
|
||||
|
||||
@@ -85,6 +85,39 @@ END:VCALENDAR`,
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With categories",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
CATEGORIES:cat1,cat2
|
||||
LAST-MODIFIED:00010101T000000
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Title: "Todo #1",
|
||||
UID: "randomuid",
|
||||
Description: "Lorem Ipsum",
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "cat1",
|
||||
},
|
||||
{
|
||||
Title: "cat2",
|
||||
},
|
||||
},
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -99,3 +132,84 @@ END:VCALENDAR`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCaldavTodosForTasks(t *testing.T) {
|
||||
type args struct {
|
||||
list *models.ListWithTasksAndBuckets
|
||||
tasks []*models.TaskWithComments
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantCaldav string
|
||||
}{
|
||||
{
|
||||
name: "Format single Task as Caldav",
|
||||
args: args{
|
||||
list: &models.ListWithTasksAndBuckets{
|
||||
List: models.List{
|
||||
Title: "List title",
|
||||
},
|
||||
},
|
||||
tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task 1",
|
||||
UID: "randomuid",
|
||||
Description: "Description",
|
||||
Priority: 3,
|
||||
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
|
||||
DueDate: time.Unix(1543626722, 0).In(config.GetTimeZone()),
|
||||
StartDate: time.Unix(1543626723, 0).In(config.GetTimeZone()),
|
||||
EndDate: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
|
||||
DoneAt: time.Unix(1543626726, 0).In(config.GetTimeZone()),
|
||||
RepeatAfter: 86400,
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
ID: 1,
|
||||
Title: "label1",
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Title: "label2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldav: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:List title
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011205Z
|
||||
SUMMARY:Task 1
|
||||
DTSTART:20181201T011203Z
|
||||
DTEND:20181201T011204Z
|
||||
DESCRIPTION:Description
|
||||
COMPLETED:20181201T011206Z
|
||||
STATUS:COMPLETED
|
||||
DUE:20181201T011202Z
|
||||
CREATED:20181201T011201Z
|
||||
PRIORITY:3
|
||||
RRULE:FREQ=SECONDLY;INTERVAL=86400
|
||||
CATEGORIES:label1,label2
|
||||
LAST-MODIFIED:20181201T011205Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetCaldavTodosForTasks(tt.args.list, tt.args.tasks)
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.wantCaldav); !equal {
|
||||
t.Errorf("GetCaldavTodosForTasks() gotVTask = %v, want %v, diff = %s", got, tt.wantCaldav, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -225,7 +225,7 @@ var userUpdateCmd = &cobra.Command{
|
||||
u.AvatarProvider = userFlagAvatar
|
||||
}
|
||||
|
||||
_, err := user.UpdateUser(s, u)
|
||||
_, err := user.UpdateUser(s, u, false)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Error updating the user: %s", err)
|
||||
@@ -299,7 +299,7 @@ var userChangeEnabledCmd = &cobra.Command{
|
||||
u.Status = user.StatusActive
|
||||
}
|
||||
}
|
||||
_, err := user.UpdateUser(s, u)
|
||||
_, err := user.UpdateUser(s, u, false)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Could not enable the user")
|
||||
|
||||
+21
-6
@@ -128,10 +128,6 @@ const (
|
||||
FilesBasePath Key = `files.basepath`
|
||||
FilesMaxSize Key = `files.maxsize`
|
||||
|
||||
MigrationWunderlistEnable Key = `migration.wunderlist.enable`
|
||||
MigrationWunderlistClientID Key = `migration.wunderlist.clientid`
|
||||
MigrationWunderlistClientSecret Key = `migration.wunderlist.clientsecret`
|
||||
MigrationWunderlistRedirectURL Key = `migration.wunderlist.redirecturl`
|
||||
MigrationTodoistEnable Key = `migration.todoist.enable`
|
||||
MigrationTodoistClientID Key = `migration.todoist.clientid`
|
||||
MigrationTodoistClientSecret Key = `migration.todoist.clientsecret`
|
||||
@@ -161,6 +157,18 @@ const (
|
||||
MetricsEnabled Key = `metrics.enabled`
|
||||
MetricsUsername Key = `metrics.username`
|
||||
MetricsPassword Key = `metrics.password`
|
||||
|
||||
DefaultSettingsAvatarProvider Key = `defaultsettings.avatar_provider`
|
||||
DefaultSettingsAvatarFileID Key = `defaultsettings.avatar_file_id`
|
||||
DefaultSettingsEmailRemindersEnabled Key = `defaultsettings.email_reminders_enabled`
|
||||
DefaultSettingsDiscoverableByName Key = `defaultsettings.discoverable_by_name`
|
||||
DefaultSettingsDiscoverableByEmail Key = `defaultsettings.discoverable_by_email`
|
||||
DefaultSettingsOverdueTaskRemindersEnabled Key = `defaultsettings.overdue_tasks_reminders_enabled`
|
||||
DefaultSettingsDefaultListID Key = `defaultsettings.default_list_id`
|
||||
DefaultSettingsWeekStart Key = `defaultsettings.week_start`
|
||||
DefaultSettingsLanguage Key = `defaultsettings.language`
|
||||
DefaultSettingsTimezone Key = `defaultsettings.timezone`
|
||||
DefaultSettingsOverdueTaskRemindersTime Key = `defaultsettings.overdue_tasks_reminders_time`
|
||||
)
|
||||
|
||||
// GetString returns a string config value
|
||||
@@ -341,7 +349,7 @@ func InitDefaultConfig() {
|
||||
LogHTTP.setDefault("stdout")
|
||||
LogEcho.setDefault("off")
|
||||
LogPath.setDefault(ServiceRootpath.GetString() + "/logs")
|
||||
LogEvents.setDefault("stdout")
|
||||
LogEvents.setDefault("off")
|
||||
LogEventsLevel.setDefault("INFO")
|
||||
// Rate Limit
|
||||
RateLimitEnabled.setDefault(false)
|
||||
@@ -357,7 +365,6 @@ func InitDefaultConfig() {
|
||||
CorsOrigins.setDefault([]string{"*"})
|
||||
CorsMaxAge.setDefault(0)
|
||||
// Migration
|
||||
MigrationWunderlistEnable.setDefault(false)
|
||||
MigrationTodoistEnable.setDefault(false)
|
||||
MigrationTrelloEnable.setDefault(false)
|
||||
MigrationMicrosoftTodoEnable.setDefault(false)
|
||||
@@ -371,6 +378,10 @@ func InitDefaultConfig() {
|
||||
KeyvalueType.setDefault("memory")
|
||||
// Metrics
|
||||
MetricsEnabled.setDefault(false)
|
||||
// Settings
|
||||
DefaultSettingsAvatarProvider.setDefault("initials")
|
||||
DefaultSettingsOverdueTaskRemindersEnabled.setDefault(true)
|
||||
DefaultSettingsOverdueTaskRemindersTime.setDefault("9:00")
|
||||
}
|
||||
|
||||
// InitConfig initializes the config, sets defaults etc.
|
||||
@@ -438,6 +449,10 @@ func InitConfig() {
|
||||
MigrationMicrosoftTodoRedirectURL.Set(ServiceFrontendurl.GetString() + "migrate/microsoft-todo")
|
||||
}
|
||||
|
||||
if DefaultSettingsTimezone.GetString() == "" {
|
||||
DefaultSettingsTimezone.Set(ServiceTimeZone.GetString())
|
||||
}
|
||||
|
||||
if ServiceEnableMetrics.GetBool() {
|
||||
log.Println("WARNING: service.enablemetrics is deprecated and will be removed in a future release. Please use metrics.enable.")
|
||||
MetricsEnabled.Set(true)
|
||||
|
||||
+22
-6
@@ -19,6 +19,7 @@ package db
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -27,9 +28,9 @@ import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
xrc "gitea.com/xorm/xorm-redis-cache"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/caches"
|
||||
"xorm.io/xorm/names"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Because.
|
||||
@@ -80,7 +81,7 @@ func CreateDBEngine() (engine *xorm.Engine, err error) {
|
||||
log.Fatalf("Error parsing time zone: %s", err)
|
||||
}
|
||||
engine.SetTZDatabase(loc)
|
||||
engine.SetMapper(core.GonicMapper{})
|
||||
engine.SetMapper(names.GonicMapper{})
|
||||
logger := log.NewXormLogger("")
|
||||
engine.SetLogger(logger)
|
||||
|
||||
@@ -147,11 +148,26 @@ func parsePostgreSQLHostPort(info string) (string, string) {
|
||||
return host, port
|
||||
}
|
||||
|
||||
// Copied and adopted from https://github.com/go-gitea/gitea/blob/f337c32e868381c6d2d948221aca0c59f8420c13/modules/setting/database.go#L176-L186
|
||||
func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbSslMode, dbSslCert, dbSslKey, dbSslRootCert string) (connStr string) {
|
||||
dbParam := "?"
|
||||
if strings.Contains(dbName, dbParam) {
|
||||
dbParam = "&"
|
||||
}
|
||||
host, port := parsePostgreSQLHostPort(dbHost)
|
||||
if host[0] == '/' { // looks like a unix socket
|
||||
connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&sslcert=%s&sslkey=%s&sslrootcert=%s&host=%s",
|
||||
url.PathEscape(dbUser), url.PathEscape(dbPasswd), port, dbName, dbParam, dbSslMode, dbSslCert, dbSslKey, dbSslRootCert, host)
|
||||
} else {
|
||||
connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s&sslcert=%s&sslkey=%s&sslrootcert=%s",
|
||||
url.PathEscape(dbUser), url.PathEscape(dbPasswd), host, port, dbName, dbParam, dbSslMode, dbSslCert, dbSslKey, dbSslRootCert)
|
||||
}
|
||||
return connStr
|
||||
}
|
||||
|
||||
func initPostgresEngine() (engine *xorm.Engine, err error) {
|
||||
host, port := parsePostgreSQLHostPort(config.DatabaseHost.GetString())
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s",
|
||||
host,
|
||||
port,
|
||||
connStr := getPostgreSQLConnectionString(
|
||||
config.DatabaseHost.GetString(),
|
||||
config.DatabaseUser.GetString(),
|
||||
config.DatabasePassword.GetString(),
|
||||
config.DatabaseDatabase.GetString(),
|
||||
|
||||
@@ -18,6 +18,7 @@ package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
@@ -53,7 +54,35 @@ func Restore(table string, contents []map[string]interface{}) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
meta, err := x.DBMetas()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var metaForCurrentTable *schemas.Table
|
||||
for _, m := range meta {
|
||||
if m.Name == table {
|
||||
metaForCurrentTable = m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if metaForCurrentTable == nil {
|
||||
log.Fatalf("Could not find table definition for table %s", table)
|
||||
}
|
||||
|
||||
for _, content := range contents {
|
||||
for colName, value := range content {
|
||||
// Date fields might get restored as 0001-01-01 from null dates. This can have unintended side-effects like
|
||||
// users being scheduled for deletion after a restore.
|
||||
// To avoid this, we set these dates to nil so that they'll end up as null in the db.
|
||||
col := metaForCurrentTable.GetColumn(colName)
|
||||
strVal, is := value.(string)
|
||||
if is && col.SQLType.IsTime() && (strVal == "" || strings.HasPrefix(strVal, "0001-")) {
|
||||
content[colName] = nil
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := x.Table(table).Insert(content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
list_id: 1
|
||||
created_by_id: 1
|
||||
limit: 9999999 # This bucket has a limit we will never exceed in the tests to make sure the logic allows for buckets with limits
|
||||
position: 2
|
||||
position: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 2
|
||||
@@ -11,7 +11,7 @@
|
||||
list_id: 1
|
||||
created_by_id: 1
|
||||
limit: 3
|
||||
position: 1
|
||||
position: 2
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 3
|
||||
@@ -218,3 +218,9 @@
|
||||
created_by_id: -2
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 36
|
||||
title: testbucket36
|
||||
list_id: 26
|
||||
created_by_id: 15
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
|
||||
@@ -14,3 +14,7 @@
|
||||
task_id: 36
|
||||
label_id: 4
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 5
|
||||
task_id: 39
|
||||
label_id: 4
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
@@ -242,3 +242,24 @@
|
||||
position: 7
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 25
|
||||
title: Test25 with background
|
||||
description: Lorem Ipsum
|
||||
identifier: test6
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
background_file_id: 1
|
||||
position: 8
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 26
|
||||
title: List 26 for Caldav tests
|
||||
description: Lorem Ipsum
|
||||
identifier: test26
|
||||
owner_id: 15
|
||||
namespace_id: 18
|
||||
position: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
@@ -88,3 +88,9 @@
|
||||
owner_id: 12
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 18
|
||||
title: testnamespace18
|
||||
description: Lorem Ipsum
|
||||
owner_id: 15
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
@@ -337,6 +337,7 @@
|
||||
bucket_id: 20
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
due_date: 2018-10-30 22:25:24
|
||||
- id: 37
|
||||
title: 'task #37'
|
||||
done: false
|
||||
@@ -354,3 +355,17 @@
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
due_date: 2018-10-30 22:25:24
|
||||
- id: 39
|
||||
uid: 'uid-caldav-test'
|
||||
title: 'Title Caldav Test'
|
||||
description: 'Description Caldav Test'
|
||||
priority: 3
|
||||
done: false
|
||||
created_by_id: 15
|
||||
list_id: 26
|
||||
index: 39
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 1
|
||||
position: 39
|
||||
|
||||
@@ -109,3 +109,10 @@
|
||||
subject: '12345'
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 15
|
||||
username: 'user15'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user15@example.com'
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
@@ -46,3 +46,9 @@
|
||||
right: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 9
|
||||
user_id: 15
|
||||
list_id: 26
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
||||
+11
-2
@@ -23,9 +23,11 @@ import (
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/core"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/names"
|
||||
)
|
||||
|
||||
// CreateTestEngine creates an instance of the db engine which lives in memory
|
||||
@@ -48,7 +50,7 @@ func CreateTestEngine() (engine *xorm.Engine, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
engine.SetMapper(core.GonicMapper{})
|
||||
engine.SetMapper(names.GonicMapper{})
|
||||
logger := log.NewXormLogger("DEBUG")
|
||||
logger.ShowSQL(os.Getenv("UNIT_TESTS_VERBOSE") == "1")
|
||||
engine.SetLogger(logger)
|
||||
@@ -101,3 +103,10 @@ func AssertMissing(t *testing.T, table string, values map[string]interface{}) {
|
||||
assert.NoError(t, err, fmt.Sprintf("Failed to assert entries don't exist in db, error was: %s", err))
|
||||
assert.False(t, exists, fmt.Sprintf("Entries %v exist in table %s", values, table))
|
||||
}
|
||||
|
||||
// AssertCount checks if a number of entries exists in the database
|
||||
func AssertCount(t *testing.T, table string, where builder.Cond, count int64) {
|
||||
dbCount, err := x.Table(table).Where(where).Count()
|
||||
assert.NoError(t, err, fmt.Sprintf("Failed to assert count in db, error was: %s", err))
|
||||
assert.Equal(t, count, dbCount, fmt.Sprintf("Found %d entries instead of expected %d in table %s", dbCount, count, table))
|
||||
}
|
||||
|
||||
+4
-4
@@ -83,7 +83,7 @@ func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, m
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
file, err = CreateWithMimeAndSession(s, f, realname, realsize, a, mime)
|
||||
file, err = CreateWithMimeAndSession(s, f, realname, realsize, a, mime, true)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return
|
||||
@@ -91,14 +91,14 @@ func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, m
|
||||
return
|
||||
}
|
||||
|
||||
func CreateWithMimeAndSession(s *xorm.Session, f io.Reader, realname string, realsize uint64, a web.Auth, mime string) (file *File, err error) {
|
||||
func CreateWithMimeAndSession(s *xorm.Session, f io.Reader, realname string, realsize uint64, a web.Auth, mime string, checkFileSizeLimit bool) (file *File, err error) {
|
||||
// Get and parse the configured file size
|
||||
var maxSize datasize.ByteSize
|
||||
err = maxSize.UnmarshalText([]byte(config.FilesMaxSize.GetString()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if realsize > maxSize.Bytes() {
|
||||
if realsize > maxSize.Bytes() && checkFileSizeLimit {
|
||||
return nil, ErrFileIsTooLarge{Size: realsize}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func (f *File) Delete() (err error) {
|
||||
var perr *os.PathError
|
||||
if errors.As(err, &perr) {
|
||||
// Don't fail when removing the file failed
|
||||
log.Errorf("Error deleting file %d: %w", err)
|
||||
log.Errorf("Error deleting file %d: %w", f.ID, err)
|
||||
return s.Commit()
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ func TestArchived(t *testing.T) {
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
|
||||
})
|
||||
t.Run("unarchivable", func(t *testing.T) {
|
||||
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false}`)
|
||||
rec, err := testListHandler.testUpdateWithUser(nil, map[string]string{"list": "22"}, `{"title":"LoremIpsum","is_archived":false,"namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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 integrations
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/routes/caldav"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const vtodo = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:List 26 for Caldav tests
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:uid
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav Task 1
|
||||
CATEGORIES:tag1,tag2,tag3
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
func TestCaldav(t *testing.T) {
|
||||
t.Run("Delivers VTODO for list", func(t *testing.T) {
|
||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ListHandler, &testuser15, ``, nil, map[string]string{"list": "26"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
|
||||
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
|
||||
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:List 26 for Caldav tests")
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
|
||||
assert.Contains(t, rec.Body.String(), "END:VTODO")
|
||||
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
|
||||
})
|
||||
t.Run("Import VTODO", func(t *testing.T) {
|
||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"list": "26", "task": "uid"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rec.Result().StatusCode, 201)
|
||||
})
|
||||
t.Run("Export VTODO", func(t *testing.T) {
|
||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"list": "26", "task": "uid-caldav-test"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
|
||||
assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test")
|
||||
assert.Contains(t, rec.Body.String(), "DESCRIPTION:Description Caldav Test")
|
||||
assert.Contains(t, rec.Body.String(), "DUE:20230301T150000Z")
|
||||
assert.Contains(t, rec.Body.String(), "PRIORITY:3")
|
||||
assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4")
|
||||
})
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"code.vikunja.io/api/pkg/routes"
|
||||
"code.vikunja.io/api/pkg/routes/caldav"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"code.vikunja.io/web/handler"
|
||||
@@ -49,30 +50,11 @@ var (
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user1@example.com",
|
||||
}
|
||||
testuser2 = user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
testuser15 = user.User{
|
||||
ID: 15,
|
||||
Username: "user15",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user2@example.com",
|
||||
}
|
||||
testuser3 = user.User{
|
||||
ID: 3,
|
||||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user3@example.com",
|
||||
}
|
||||
testuser4 = user.User{
|
||||
ID: 4,
|
||||
Username: "user4",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user4@example.com",
|
||||
}
|
||||
testuser5 = user.User{
|
||||
ID: 4,
|
||||
Username: "user5",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user5@example.com",
|
||||
Status: user.StatusDisabled,
|
||||
Email: "user15@example.com",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -170,6 +152,19 @@ func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.Handl
|
||||
return
|
||||
}
|
||||
|
||||
func newCaldavTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
|
||||
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextPlain)
|
||||
|
||||
result, _ := caldav.BasicAuth(user.Username, "1234", c)
|
||||
if !result {
|
||||
t.Error("BasicAuth for caldav failed")
|
||||
t.FailNow()
|
||||
}
|
||||
err = handler(c)
|
||||
return
|
||||
}
|
||||
|
||||
func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) {
|
||||
if err == nil {
|
||||
t.Error("Error is nil")
|
||||
|
||||
@@ -232,12 +232,12 @@ func TestLinkSharing(t *testing.T) {
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum","namespace_id":2}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestList(t *testing.T) {
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
// Check the list was loaded successfully afterwards, see testReadOneWithUser
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
// The description should not be updated but returned correctly
|
||||
@@ -183,7 +183,7 @@ func TestList(t *testing.T) {
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
})
|
||||
t.Run("Normal with updating the description", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet","namespace_id":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
|
||||
@@ -211,12 +211,12 @@ func TestList(t *testing.T) {
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
@@ -227,12 +227,12 @@ func TestList(t *testing.T) {
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum","namespace_id":6}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
@@ -243,12 +243,12 @@ func TestList(t *testing.T) {
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum","namespace_id":8}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum","namespace_id":9}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
@@ -259,12 +259,12 @@ func TestList(t *testing.T) {
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum","namespace_id":11}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum","namespace_id":12}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
|
||||
})
|
||||
|
||||
@@ -113,49 +113,49 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("by priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
// Due date without unix suffix
|
||||
t.Run("by duedate asc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by due_date without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc without suffix", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
@@ -366,33 +366,33 @@ func TestTaskCollection(t *testing.T) {
|
||||
t.Run("by priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
|
||||
+9
-4
@@ -55,6 +55,11 @@ func getClient() (*mail.Client, error) {
|
||||
InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool(),
|
||||
ServerName: config.MailerHost.GetString(),
|
||||
}),
|
||||
mail.WithTimeout((config.MailerQueueTimeout.GetDuration() + 3) * time.Second), // 3s more for us to close before mail server timeout
|
||||
}
|
||||
|
||||
if config.MailerForceSSL.GetBool() {
|
||||
opts = append(opts, mail.WithSSL())
|
||||
}
|
||||
|
||||
if config.MailerUsername.GetString() != "" && config.MailerPassword.GetString() != "" {
|
||||
@@ -105,14 +110,14 @@ func StartMailDaemon() {
|
||||
if !open {
|
||||
err = c.DialWithContext(context.Background())
|
||||
if err != nil {
|
||||
log.Error("Error during connect to smtp server: %s", err)
|
||||
log.Errorf("Error during connect to smtp server: %s", err)
|
||||
break
|
||||
}
|
||||
open = true
|
||||
}
|
||||
err = c.Send(m)
|
||||
if err != nil {
|
||||
log.Error("Error when sending mail: %s", err)
|
||||
log.Errorf("Error when sending mail: %s", err)
|
||||
break
|
||||
}
|
||||
// Close the connection to the SMTP server if no email was sent in
|
||||
@@ -122,10 +127,10 @@ func StartMailDaemon() {
|
||||
open = false
|
||||
err = c.Close()
|
||||
if err != nil {
|
||||
log.Error("Error closing the mail server connection: %s\n", err)
|
||||
log.Errorf("Error closing the mail server connection: %s\n", err)
|
||||
break
|
||||
}
|
||||
log.Infof("Closed connection to mail server")
|
||||
log.Info("Closed connection to mail server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func getMessage(opts *Opts) *mail.Msg {
|
||||
m.Subject(opts.Subject)
|
||||
|
||||
for _, h := range opts.Headers {
|
||||
m.SetHeader(h.Field, h.Content)
|
||||
m.SetGenHeader(h.Field, h.Content)
|
||||
}
|
||||
|
||||
for name, content := range opts.Embeds {
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
// SecondsUntilInactive defines the seconds until a user is considered inactive
|
||||
const SecondsUntilInactive = 60
|
||||
const SecondsUntilInactive = 30
|
||||
|
||||
// ActiveUsersKey is the key used to store active users in redis
|
||||
const ActiveUsersKey = `activeusers`
|
||||
@@ -55,12 +55,13 @@ func init() {
|
||||
users: make(map[int64]*ActiveUser),
|
||||
mutex: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
func setupActiveUsersMetric() {
|
||||
err := registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_active_users",
|
||||
Help: "The currently active users on this node",
|
||||
Help: "The number of users active within the last 30 seconds on this node",
|
||||
}, func() float64 {
|
||||
|
||||
allActiveUsers, err := getActiveUsers()
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
@@ -75,7 +76,10 @@ func init() {
|
||||
}
|
||||
}
|
||||
return float64(activeUsersCount)
|
||||
})
|
||||
}))
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for currently active users: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SetUserActive sets a user as active and pushes it to redis
|
||||
|
||||
@@ -113,7 +113,7 @@ func InitMetrics() {
|
||||
log.Criticalf("Could not register metrics for %s: %s", TaskCountKey, err)
|
||||
}
|
||||
|
||||
// Register total user count metric
|
||||
// Register total teams count metric
|
||||
err = registry.Register(promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_team_count",
|
||||
Help: "The total number of teams on this instance",
|
||||
@@ -124,9 +124,11 @@ func InitMetrics() {
|
||||
if err != nil {
|
||||
log.Criticalf("Could not register metrics for %s: %s", TeamCountKey, err)
|
||||
}
|
||||
|
||||
setupActiveUsersMetric()
|
||||
}
|
||||
|
||||
// GetCount returns the current count from redis
|
||||
// GetCount returns the current count from keyvalue
|
||||
func GetCount(key string) (count int64, err error) {
|
||||
cnt, exists, err := keyvalue.Get(key)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type tasks20221002120521 struct {
|
||||
CoverImageAttachmentID int64 `xorm:"bigint default 0" json:"cover_image_attachment_id"`
|
||||
}
|
||||
|
||||
func (tasks20221002120521) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20221002120521",
|
||||
Description: "Add cover image attachment id",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(tasks20221002120521{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -283,6 +283,34 @@ func (err *ErrListCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrListMustBelongToANamespace represents an error where a list must belong to a namespace
|
||||
type ErrListMustBelongToANamespace struct {
|
||||
ListID int64
|
||||
NamespaceID int64
|
||||
}
|
||||
|
||||
// IsErrListMustBelongToANamespace checks if an error is a list must belong to a namespace error.
|
||||
func IsErrListMustBelongToANamespace(err error) bool {
|
||||
_, ok := err.(*ErrListMustBelongToANamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrListMustBelongToANamespace) Error() string {
|
||||
return fmt.Sprintf("List must belong to a namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
|
||||
}
|
||||
|
||||
// ErrCodeListMustBelongToANamespace holds the unique world-error code of this error
|
||||
const ErrCodeListMustBelongToANamespace = 3010
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrListMustBelongToANamespace) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeListMustBelongToANamespace,
|
||||
Message: "This list must belong to a namespace.",
|
||||
}
|
||||
}
|
||||
|
||||
// ================
|
||||
// List task errors
|
||||
// ================
|
||||
@@ -791,6 +819,62 @@ func (err ErrInvalidTaskFilterValue) HTTPError() web.HTTPError {
|
||||
}
|
||||
}
|
||||
|
||||
// ErrAttachmentDoesNotBelongToTask represents an error where the provided task cover attachment does not belong to the same task
|
||||
type ErrAttachmentDoesNotBelongToTask struct {
|
||||
TaskID int64
|
||||
AttachmentID int64
|
||||
}
|
||||
|
||||
// IsErrAttachmentAndCoverMustBelongToTheSameTask checks if an error is ErrAttachmentDoesNotBelongToTask.
|
||||
func IsErrAttachmentAndCoverMustBelongToTheSameTask(err error) bool {
|
||||
_, ok := err.(ErrAttachmentDoesNotBelongToTask)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrAttachmentDoesNotBelongToTask) Error() string {
|
||||
return fmt.Sprintf("Task attachment and cover image do not belong to the same task [TaskID: %d, AttachmentID: %d]", err.TaskID, err.AttachmentID)
|
||||
}
|
||||
|
||||
// ErrCodeAttachmentDoesNotBelongToTask holds the unique world-error code of this error
|
||||
const ErrCodeAttachmentDoesNotBelongToTask = 4020
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrAttachmentDoesNotBelongToTask) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeAttachmentDoesNotBelongToTask,
|
||||
Message: "This attachment does not belong to that task.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrUserAlreadyAssigned represents an error where the user is already assigned to this task
|
||||
type ErrUserAlreadyAssigned struct {
|
||||
TaskID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// IsErrUserAlreadyAssigned checks if an error is ErrUserAlreadyAssigned.
|
||||
func IsErrUserAlreadyAssigned(err error) bool {
|
||||
_, ok := err.(ErrUserAlreadyAssigned)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserAlreadyAssigned) Error() string {
|
||||
return fmt.Sprintf("User is already assigned to task [TaskID: %d, UserID: %d]", err.TaskID, err.UserID)
|
||||
}
|
||||
|
||||
// ErrCodeUserAlreadyAssigned holds the unique world-error code of this error
|
||||
const ErrCodeUserAlreadyAssigned = 4021
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserAlreadyAssigned) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeUserAlreadyAssigned,
|
||||
Message: "This user is already assigned to that task.",
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
||||
@@ -80,6 +80,18 @@ func (t *TaskAssigneeCreatedEvent) Name() string {
|
||||
return "task.assignee.created"
|
||||
}
|
||||
|
||||
// TaskAssigneeDeletedEvent represents a TaskAssigneeDeletedEvent event
|
||||
type TaskAssigneeDeletedEvent struct {
|
||||
Task *Task
|
||||
Assignee *user.User
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAssigneeDeletedEvent
|
||||
func (t *TaskAssigneeDeletedEvent) Name() string {
|
||||
return "task.assignee.deleted"
|
||||
}
|
||||
|
||||
// TaskCommentCreatedEvent represents an event where a task comment has been created
|
||||
type TaskCommentCreatedEvent struct {
|
||||
Task *Task
|
||||
@@ -104,6 +116,66 @@ func (t *TaskCommentUpdatedEvent) Name() string {
|
||||
return "task.comment.edited"
|
||||
}
|
||||
|
||||
// TaskCommentDeletedEvent represents a TaskCommentDeletedEvent event
|
||||
type TaskCommentDeletedEvent struct {
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCommentDeletedEvent
|
||||
func (t *TaskCommentDeletedEvent) Name() string {
|
||||
return "task.comment.deleted"
|
||||
}
|
||||
|
||||
// TaskAttachmentCreatedEvent represents a TaskAttachmentCreatedEvent event
|
||||
type TaskAttachmentCreatedEvent struct {
|
||||
Task *Task
|
||||
Attachment *TaskAttachment
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAttachmentCreatedEvent
|
||||
func (t *TaskAttachmentCreatedEvent) Name() string {
|
||||
return "task.attachment.created"
|
||||
}
|
||||
|
||||
// TaskAttachmentDeletedEvent represents a TaskAttachmentDeletedEvent event
|
||||
type TaskAttachmentDeletedEvent struct {
|
||||
Task *Task
|
||||
Attachment *TaskAttachment
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskAttachmentDeletedEvent
|
||||
func (t *TaskAttachmentDeletedEvent) Name() string {
|
||||
return "task.attachment.deleted"
|
||||
}
|
||||
|
||||
// TaskRelationCreatedEvent represents a TaskRelationCreatedEvent event
|
||||
type TaskRelationCreatedEvent struct {
|
||||
Task *Task
|
||||
Relation *TaskRelation
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskRelationCreatedEvent
|
||||
func (t *TaskRelationCreatedEvent) Name() string {
|
||||
return "task.relation.created"
|
||||
}
|
||||
|
||||
// TaskRelationDeletedEvent represents a TaskRelationDeletedEvent event
|
||||
type TaskRelationDeletedEvent struct {
|
||||
Task *Task
|
||||
Relation *TaskRelation
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskRelationDeletedEvent
|
||||
func (t *TaskRelationDeletedEvent) Name() string {
|
||||
return "task.relation.deleted"
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Namespace Events //
|
||||
//////////////////////
|
||||
|
||||
@@ -97,7 +97,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
exportFile, err := files.CreateWithMimeAndSession(s, exported, tmpFilename, uint64(stat.Size()), u, "application/zip")
|
||||
exportFile, err := files.CreateWithMimeAndSession(s, exported, tmpFilename, uint64(stat.Size()), u, "application/zip", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func getDefaultBucket(s *xorm.Session, listID int64) (bucket *Bucket, err error)
|
||||
bucket = &Bucket{}
|
||||
_, err = s.
|
||||
Where("list_id = ?", listID).
|
||||
OrderBy("id asc").
|
||||
OrderBy("position asc").
|
||||
Get(bucket)
|
||||
return
|
||||
}
|
||||
|
||||
+12
-12
@@ -49,22 +49,22 @@ func TestBucket_ReadAll(t *testing.T) {
|
||||
assert.Len(t, buckets, 3)
|
||||
|
||||
// Assert all tasks are in the right bucket
|
||||
assert.Len(t, buckets[0].Tasks, 3)
|
||||
assert.Len(t, buckets[1].Tasks, 12)
|
||||
assert.Len(t, buckets[0].Tasks, 12)
|
||||
assert.Len(t, buckets[1].Tasks, 3)
|
||||
assert.Len(t, buckets[2].Tasks, 3)
|
||||
|
||||
// Assert we have bucket 1, 2, 3 but not 4 (that belongs to a different list) and their position
|
||||
assert.Equal(t, int64(2), buckets[0].ID)
|
||||
assert.Equal(t, int64(1), buckets[1].ID)
|
||||
assert.Equal(t, int64(1), buckets[0].ID)
|
||||
assert.Equal(t, int64(2), buckets[1].ID)
|
||||
assert.Equal(t, int64(3), buckets[2].ID)
|
||||
|
||||
// Kinda assert all tasks are in the right buckets
|
||||
assert.Equal(t, int64(1), buckets[1].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[1].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[0].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[0].Tasks[1].BucketID)
|
||||
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[2].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[2].BucketID)
|
||||
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[1].BucketID)
|
||||
@@ -89,8 +89,8 @@ func TestBucket_ReadAll(t *testing.T) {
|
||||
|
||||
buckets := bucketsInterface.([]*Bucket)
|
||||
assert.Len(t, buckets, 3)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[0].ID)
|
||||
assert.Equal(t, int64(33), buckets[1].Tasks[1].ID)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[0].ID)
|
||||
assert.Equal(t, int64(33), buckets[0].Tasks[1].ID)
|
||||
})
|
||||
t.Run("accessed by link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
@@ -147,7 +147,7 @@ func TestBucket_Delete(t *testing.T) {
|
||||
tasks := []*Task{}
|
||||
err = s.Where("bucket_id = ?", 1).Find(&tasks)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tasks, 15)
|
||||
assert.Len(t, tasks, 16)
|
||||
db.AssertMissing(t, "buckets", map[string]interface{}{
|
||||
"id": 2,
|
||||
"list_id": 1,
|
||||
|
||||
+12
-10
@@ -151,8 +151,8 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
return getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
Search: search,
|
||||
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
Search: []string{search},
|
||||
User: u,
|
||||
GetForUser: u.ID,
|
||||
Page: page,
|
||||
@@ -178,13 +178,13 @@ func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, pe
|
||||
func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
label, err := getLabelByIDSimple(s, l.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
*l = *label
|
||||
|
||||
u, err := user.GetUserByID(s, l.CreatedByID)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
l.CreatedBy = u
|
||||
@@ -192,14 +192,16 @@ func (l *Label) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
func getLabelByIDSimple(s *xorm.Session, labelID int64) (*Label, error) {
|
||||
label := Label{}
|
||||
exists, err := s.ID(labelID).Get(&label)
|
||||
return GetLabelSimple(s, &Label{ID: labelID})
|
||||
}
|
||||
|
||||
func GetLabelSimple(s *xorm.Session, l *Label) (*Label, error) {
|
||||
exists, err := s.Get(l)
|
||||
if err != nil {
|
||||
return &label, err
|
||||
return l, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return &Label{}, ErrLabelDoesNotExist{labelID}
|
||||
return &Label{}, ErrLabelDoesNotExist{l.ID}
|
||||
}
|
||||
return &label, err
|
||||
return l, err
|
||||
}
|
||||
|
||||
+32
-16
@@ -129,16 +129,16 @@ func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page in
|
||||
return nil, 0, 0, ErrNoRightToSeeTask{lt.TaskID, a.GetID()}
|
||||
}
|
||||
|
||||
return getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
User: &user.User{ID: a.GetID()},
|
||||
Search: search,
|
||||
Search: []string{search},
|
||||
Page: page,
|
||||
TaskIDs: []int64{lt.TaskID},
|
||||
})
|
||||
}
|
||||
|
||||
// Helper struct, contains the label + its task ID
|
||||
type labelWithTaskID struct {
|
||||
// LabelWithTaskID is a helper struct, contains the label + its task ID
|
||||
type LabelWithTaskID struct {
|
||||
TaskID int64 `json:"-"`
|
||||
Label `xorm:"extends"`
|
||||
}
|
||||
@@ -146,7 +146,7 @@ type labelWithTaskID struct {
|
||||
// LabelByTaskIDsOptions is a struct to not clutter the function with too many optional parameters.
|
||||
type LabelByTaskIDsOptions struct {
|
||||
User *user.User
|
||||
Search string
|
||||
Search []string
|
||||
Page int
|
||||
PerPage int
|
||||
TaskIDs []int64
|
||||
@@ -155,9 +155,9 @@ type LabelByTaskIDsOptions struct {
|
||||
GetForUser int64
|
||||
}
|
||||
|
||||
// Helper function to get all labels for a set of tasks
|
||||
// GetLabelsByTaskIDs is a helper function to get all labels for a set of tasks
|
||||
// Used when getting all labels for one task as well when getting all lables
|
||||
func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, resultCount int, totalEntries int64, err error) {
|
||||
func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*LabelWithTaskID, resultCount int, totalEntries int64, err error) {
|
||||
// We still need the task ID when we want to get all labels for a task, but because of this, we get the same label
|
||||
// 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.
|
||||
@@ -170,7 +170,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
||||
}
|
||||
|
||||
// Get all labels associated with these tasks
|
||||
var labels []*labelWithTaskID
|
||||
var labels []*LabelWithTaskID
|
||||
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)
|
||||
@@ -188,8 +188,14 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
||||
}
|
||||
|
||||
ids := []int64{}
|
||||
if opts.Search != "" {
|
||||
vals := strings.Split(opts.Search, ",")
|
||||
|
||||
for _, search := range opts.Search {
|
||||
search = strings.Trim(search, " ")
|
||||
if search == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
vals := strings.Split(search, ",")
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
@@ -202,8 +208,19 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
||||
|
||||
if len(ids) > 0 {
|
||||
cond = builder.And(cond, builder.In("labels.id", ids))
|
||||
} else {
|
||||
cond = builder.And(cond, db.ILIKE("labels.title", opts.Search))
|
||||
} else if len(opts.Search) > 0 {
|
||||
|
||||
var searchcond builder.Cond
|
||||
for _, search := range opts.Search {
|
||||
search = strings.Trim(search, " ")
|
||||
if search == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
searchcond = builder.Or(searchcond, db.ILIKE("labels.title", search))
|
||||
}
|
||||
|
||||
cond = builder.And(cond, searchcond)
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.Page, opts.PerPage)
|
||||
@@ -254,7 +271,6 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
||||
Select("count(DISTINCT labels.id)").
|
||||
Join("LEFT", "label_tasks", "label_tasks.label_id = labels.id").
|
||||
Where(cond).
|
||||
And("labels.title LIKE ?", "%"+opts.Search+"%").
|
||||
Count(&Label{})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
@@ -264,7 +280,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
||||
}
|
||||
|
||||
// Create or update a bunch of task labels
|
||||
func (t *Task) updateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Label) (err error) {
|
||||
func (t *Task) UpdateTaskLabels(s *xorm.Session, creator web.Auth, labels []*Label) (err error) {
|
||||
|
||||
// If we don't have any new labels, delete everything right away. Saves us some hassle.
|
||||
if len(labels) == 0 && len(t.Labels) > 0 {
|
||||
@@ -381,7 +397,7 @@ func (ltb *LabelTaskBulk) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
labels, _, _, err := getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
labels, _, _, err := GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
TaskIDs: []int64{ltb.TaskID},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -390,5 +406,5 @@ func (ltb *LabelTaskBulk) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
for _, l := range labels {
|
||||
task.Labels = append(task.Labels, &l.Label)
|
||||
}
|
||||
return task.updateTaskLabels(s, a, ltb.Labels)
|
||||
return task.UpdateTaskLabels(s, a, ltb.Labels)
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
||||
args: args{
|
||||
a: &user.User{ID: 1},
|
||||
},
|
||||
wantLabels: []*labelWithTaskID{
|
||||
wantLabels: []*LabelWithTaskID{
|
||||
{
|
||||
TaskID: 1,
|
||||
Label: label,
|
||||
@@ -116,7 +116,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
||||
a: &user.User{ID: 1},
|
||||
search: "VISIBLE",
|
||||
},
|
||||
wantLabels: []*labelWithTaskID{
|
||||
wantLabels: []*LabelWithTaskID{
|
||||
{
|
||||
TaskID: 1,
|
||||
Label: label,
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
||||
args: args{
|
||||
a: &user.User{ID: 1},
|
||||
},
|
||||
wantLs: []*labelWithTaskID{
|
||||
wantLs: []*LabelWithTaskID{
|
||||
{
|
||||
Label: Label{
|
||||
ID: 1,
|
||||
|
||||
+39
-1
@@ -156,7 +156,7 @@ func GetListsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (lists [
|
||||
Alias("l").
|
||||
Join("LEFT", []string{"namespaces", "n"}, "l.namespace_id = n.id").
|
||||
Where("l.is_archived = false").
|
||||
Where("n.is_archived = false").
|
||||
Where("n.is_archived = false OR n.is_archived IS NULL").
|
||||
Where("namespace_id = ?", nID).
|
||||
Find(&lists)
|
||||
}
|
||||
@@ -476,12 +476,22 @@ func addListDetails(s *xorm.Session, lists []*List, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
subscriptions, err := GetSubscriptions(s, SubscriptionEntityList, listIDs, a)
|
||||
if err != nil {
|
||||
log.Errorf("An error occurred while getting list subscriptions for a namespace item: %s", err.Error())
|
||||
subscriptions = make(map[int64]*Subscription)
|
||||
}
|
||||
|
||||
for _, list := range lists {
|
||||
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
|
||||
if list.IsFavorite {
|
||||
continue
|
||||
}
|
||||
list.IsFavorite = favs[list.ID]
|
||||
|
||||
if subscription, exists := subscriptions[list.ID]; exists {
|
||||
list.Subscription = subscription
|
||||
}
|
||||
}
|
||||
|
||||
if len(fileIDs) == 0 {
|
||||
@@ -630,6 +640,13 @@ func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground
|
||||
return
|
||||
}
|
||||
|
||||
if list.NamespaceID == 0 {
|
||||
return &ErrListMustBelongToANamespace{
|
||||
ListID: list.ID,
|
||||
NamespaceID: list.NamespaceID,
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
@@ -778,6 +795,11 @@ func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /lists/{id} [delete]
|
||||
func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
fullList, err := GetListSimpleByID(s, l.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the list
|
||||
_, err = s.ID(l.ID).Delete(&List{})
|
||||
if err != nil {
|
||||
@@ -798,12 +820,28 @@ func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
err = fullList.DeleteBackgroundFileIfExists()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListDeletedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteBackgroundFileIfExists deletes the list's background file from the db and the filesystem,
|
||||
// if one exists
|
||||
func (l *List) DeleteBackgroundFileIfExists() (err error) {
|
||||
if l.BackgroundFileID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
file := files.File{ID: l.BackgroundFileID}
|
||||
return file.Delete()
|
||||
}
|
||||
|
||||
// SetListBackground sets a background file as list background in the db
|
||||
func SetListBackground(s *xorm.Session, listID int64, background *files.File, blurHash string) (err error) {
|
||||
l := &List{
|
||||
|
||||
@@ -66,6 +66,7 @@ func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool,
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list or namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/duplicate [put]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ func (l *List) isOwner(u *user.User) bool {
|
||||
func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, int, error) {
|
||||
|
||||
/*
|
||||
The following loop creates an sql condition like this one:
|
||||
The following loop creates a sql condition like this one:
|
||||
|
||||
(ul.user_id = 1 AND ul.right = 1) OR (un.user_id = 1 AND un.right = 1) OR
|
||||
(tm.user_id = 1 AND tn.right = 1) OR (tm2.user_id = 1 AND tl.right = 1) OR
|
||||
@@ -242,16 +242,19 @@ func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, i
|
||||
conds = append(conds, builder.Eq{"n.owner_id": a.GetID()})
|
||||
|
||||
type allListRights struct {
|
||||
UserNamespace NamespaceUser `xorm:"extends"`
|
||||
UserList ListUser `xorm:"extends"`
|
||||
UserNamespace *NamespaceUser `xorm:"extends"`
|
||||
UserList *ListUser `xorm:"extends"`
|
||||
|
||||
TeamNamespace TeamNamespace `xorm:"extends"`
|
||||
TeamList TeamList `xorm:"extends"`
|
||||
TeamNamespace *TeamNamespace `xorm:"extends"`
|
||||
TeamList *TeamList `xorm:"extends"`
|
||||
|
||||
NamespaceOwnerID int64 `xorm:"namespaces_owner_id"`
|
||||
}
|
||||
|
||||
r := &allListRights{}
|
||||
var maxRight = 0
|
||||
exists, err := s.
|
||||
Select("l.*, un.right, ul.right, tn.right, tl.right, n.owner_id as namespaces_owner_id").
|
||||
Table("lists").
|
||||
Alias("l").
|
||||
// User stuff
|
||||
@@ -285,6 +288,9 @@ func (l *List) checkRight(s *xorm.Session, a web.Auth, rights ...Right) (bool, i
|
||||
if int(r.TeamList.Right) > maxRight {
|
||||
maxRight = int(r.TeamList.Right)
|
||||
}
|
||||
if r.NamespaceOwnerID == a.GetID() {
|
||||
maxRight = int(RightAdmin)
|
||||
}
|
||||
|
||||
return exists, maxRight, err
|
||||
}
|
||||
|
||||
+74
-13
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -140,8 +141,9 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 99999999,
|
||||
Title: "test",
|
||||
ID: 99999999,
|
||||
Title: "test",
|
||||
NamespaceID: 1,
|
||||
}
|
||||
err := list.Update(s, usr)
|
||||
assert.Error(t, err)
|
||||
@@ -245,17 +247,76 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestList_Delete(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 1,
|
||||
}
|
||||
err := list.Delete(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "lists", map[string]interface{}{
|
||||
"id": 1,
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 1,
|
||||
}
|
||||
err := list.Delete(s, &user.User{ID: 1})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "lists", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
})
|
||||
t.Run("with background", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
files.InitTestFileFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 25,
|
||||
}
|
||||
err := list.Delete(s, &user.User{ID: 6})
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertMissing(t, "lists", map[string]interface{}{
|
||||
"id": 25,
|
||||
})
|
||||
db.AssertMissing(t, "files", map[string]interface{}{
|
||||
"id": 1,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestList_DeleteBackgroundFileIfExists(t *testing.T) {
|
||||
t.Run("list with background", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
files.InitTestFileFixtures(t)
|
||||
s := db.NewSession()
|
||||
file := &files.File{ID: 1}
|
||||
list := List{
|
||||
ID: 1,
|
||||
BackgroundFileID: file.ID,
|
||||
}
|
||||
err := SetListBackground(s, list.ID, file, "")
|
||||
assert.NoError(t, err)
|
||||
err = list.DeleteBackgroundFileIfExists()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("list with invalid background", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
files.InitTestFileFixtures(t)
|
||||
s := db.NewSession()
|
||||
file := &files.File{ID: 9999}
|
||||
list := List{
|
||||
ID: 1,
|
||||
BackgroundFileID: file.ID,
|
||||
}
|
||||
err := SetListBackground(s, list.ID, file, "")
|
||||
assert.NoError(t, err)
|
||||
err = list.DeleteBackgroundFileIfExists()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, files.IsErrFileDoesNotExist(err))
|
||||
})
|
||||
t.Run("list without background", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
files.InitTestFileFixtures(t)
|
||||
list := List{ID: 1}
|
||||
err := list.DeleteBackgroundFileIfExists()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,15 @@ func RegisterListeners() {
|
||||
events.RegisterListener((&TaskCreatedEvent{}).Name(), &HandleTaskCreateMentions{})
|
||||
events.RegisterListener((&TaskUpdatedEvent{}).Name(), &HandleTaskUpdatedMentions{})
|
||||
events.RegisterListener((&UserDataExportRequestedEvent{}).Name(), &HandleUserDataExport{})
|
||||
events.RegisterListener((&TaskCommentCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskCommentUpdatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskCommentDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskAssigneeDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskAttachmentCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskAttachmentDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskRelationCreatedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
events.RegisterListener((&TaskRelationDeletedEvent{}).Name(), &HandleTaskUpdateLastUpdated{})
|
||||
}
|
||||
|
||||
//////
|
||||
@@ -403,9 +412,62 @@ func (s *HandleTaskUpdatedMentions) Handle(msg *message.Message) (err error) {
|
||||
Doer: event.Doer,
|
||||
IsNew: false,
|
||||
}
|
||||
|
||||
_, err = notifyMentionedUsers(sess, event.Task, event.Task.Description, n)
|
||||
return err
|
||||
}
|
||||
|
||||
// HandleTaskUpdateLastUpdated represents a listener
|
||||
type HandleTaskUpdateLastUpdated struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the HandleTaskUpdateLastUpdated listener
|
||||
func (s *HandleTaskUpdateLastUpdated) Name() string {
|
||||
return "handle.task.update.last.updated"
|
||||
}
|
||||
|
||||
// Handle is executed when the event HandleTaskUpdateLastUpdated listens on is fired
|
||||
func (s *HandleTaskUpdateLastUpdated) Handle(msg *message.Message) (err error) {
|
||||
// Using a map here allows us to plug this listener to all kinds of task events
|
||||
event := map[string]interface{}{}
|
||||
err = json.Unmarshal(msg.Payload, &event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task, is := event["Task"].(map[string]interface{})
|
||||
if !is {
|
||||
log.Errorf("Event payload does not contain task ID")
|
||||
return
|
||||
}
|
||||
|
||||
taskID, is := task["id"]
|
||||
if !is {
|
||||
log.Errorf("Event payload does not contain a valid task ID")
|
||||
return
|
||||
}
|
||||
|
||||
var taskIDInt int64
|
||||
switch v := taskID.(type) {
|
||||
case int64:
|
||||
taskIDInt = v
|
||||
case int:
|
||||
taskIDInt = int64(v)
|
||||
case int32:
|
||||
taskIDInt = int64(v)
|
||||
case float64:
|
||||
taskIDInt = int64(v)
|
||||
case float32:
|
||||
taskIDInt = int64(v)
|
||||
default:
|
||||
log.Errorf("Event payload does not contain a valid task ID")
|
||||
return
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
return updateTaskLastUpdated(sess, &Task{ID: taskIDInt})
|
||||
}
|
||||
|
||||
///////
|
||||
|
||||
+11
-3
@@ -198,6 +198,11 @@ func makeNamespaceSlice(namespaces map[int64]*NamespaceWithLists, userMap map[in
|
||||
n.Owner = userMap[n.OwnerID]
|
||||
n.Subscription = subscriptions[n.ID]
|
||||
all = append(all, n)
|
||||
for _, l := range n.Lists {
|
||||
if n.Subscription != nil && l.Subscription == nil {
|
||||
l.Subscription = n.Subscription
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(all, func(i, j int) bool {
|
||||
return all[i].ID < all[j].ID
|
||||
@@ -335,7 +340,7 @@ func getListsForNamespaces(s *xorm.Session, namespaceIDs []int64, archived bool)
|
||||
func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User) (sharedListsNamespace *NamespaceWithLists, err error) {
|
||||
// Create our pseudo namespace to hold the shared lists
|
||||
sharedListsPseudonamespace := SharedListsPseudoNamespace
|
||||
sharedListsPseudonamespace.Owner = doer
|
||||
sharedListsPseudonamespace.OwnerID = doer.ID
|
||||
sharedListsNamespace = &NamespaceWithLists{
|
||||
sharedListsPseudonamespace,
|
||||
[]*List{},
|
||||
@@ -385,12 +390,13 @@ func getSharedListsInNamespace(s *xorm.Session, archived bool, doer *user.User)
|
||||
func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer *user.User) (favoriteNamespace *NamespaceWithLists, err error) {
|
||||
// Create our pseudo namespace with favorite lists
|
||||
pseudoFavoriteNamespace := FavoritesPseudoNamespace
|
||||
pseudoFavoriteNamespace.Owner = doer
|
||||
pseudoFavoriteNamespace.OwnerID = doer.ID
|
||||
favoriteNamespace = &NamespaceWithLists{
|
||||
Namespace: pseudoFavoriteNamespace,
|
||||
Lists: []*List{{}},
|
||||
}
|
||||
*favoriteNamespace.Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
|
||||
favoriteNamespace.Lists[0].Owner = doer
|
||||
|
||||
for _, list := range lists {
|
||||
if !list.IsFavorite {
|
||||
@@ -448,7 +454,7 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
|
||||
}
|
||||
|
||||
savedFiltersPseudoNamespace := SavedFiltersPseudoNamespace
|
||||
savedFiltersPseudoNamespace.Owner = doer
|
||||
savedFiltersPseudoNamespace.OwnerID = doer.ID
|
||||
savedFiltersNamespace = &NamespaceWithLists{
|
||||
Namespace: savedFiltersPseudoNamespace,
|
||||
Lists: make([]*List, 0, len(savedFilters)),
|
||||
@@ -479,6 +485,7 @@ func getSavedFilters(s *xorm.Session, doer *user.User) (savedFiltersNamespace *N
|
||||
// @Success 200 {array} models.NamespaceWithLists "The Namespaces."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces [get]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
@@ -517,6 +524,7 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
ownerMap[doer.ID] = doer
|
||||
|
||||
if n.NamespacesOnly {
|
||||
all := makeNamespaceSlice(namespaces, ownerMap, subscriptionsMap)
|
||||
|
||||
@@ -18,6 +18,7 @@ package models
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -230,14 +231,23 @@ func (n *UndoneTaskOverdueNotification) Name() string {
|
||||
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
|
||||
type UndoneTasksOverdueNotification struct {
|
||||
User *user.User
|
||||
Tasks []*Task
|
||||
Tasks map[int64]*Task
|
||||
}
|
||||
|
||||
// ToMail returns the mail notification for UndoneTasksOverdueNotification
|
||||
func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
|
||||
|
||||
overdueLine := ""
|
||||
sortedTasks := make([]*Task, 0, len(n.Tasks))
|
||||
for _, task := range n.Tasks {
|
||||
sortedTasks = append(sortedTasks, task)
|
||||
}
|
||||
|
||||
sort.Slice(sortedTasks, func(i, j int) bool {
|
||||
return sortedTasks[i].DueDate.Before(sortedTasks[j].DueDate)
|
||||
})
|
||||
|
||||
overdueLine := ""
|
||||
for _, task := range sortedTasks {
|
||||
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
|
||||
overdueLine += `* [` + task.Title + `](` + config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `), overdue since ` + utils.HumanizeDuration(until) + "\n"
|
||||
}
|
||||
|
||||
+35
-10
@@ -228,28 +228,53 @@ func getSubscriberCondForEntity(entityType SubscriptionEntityType, entityID int6
|
||||
// that task, if there is none it will look for a subscription on the list the task belongs to and if that also
|
||||
// doesn't exist it will check for a subscription for the namespace the list is belonging to.
|
||||
func GetSubscription(s *xorm.Session, entityType SubscriptionEntityType, entityID int64, a web.Auth) (subscription *Subscription, err error) {
|
||||
subs, err := GetSubscriptions(s, entityType, []int64{entityID}, a)
|
||||
if err != nil || len(subs) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
if sub, exists := subs[entityID]; exists {
|
||||
return sub, nil // Take exact match first, if available
|
||||
}
|
||||
for _, sub := range subs {
|
||||
return sub, nil // For parents, take next available
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetSubscriptions returns a map of subscriptions to a set of given entity IDs
|
||||
func GetSubscriptions(s *xorm.Session, entityType SubscriptionEntityType, entityIDs []int64, a web.Auth) (listsToSubscriptions map[int64]*Subscription, err error) {
|
||||
u, is := a.(*user.User)
|
||||
if !is {
|
||||
return
|
||||
}
|
||||
|
||||
if err := entityType.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscription = &Subscription{}
|
||||
cond := getSubscriberCondForEntity(entityType, entityID)
|
||||
exists, err := s.
|
||||
var entitiesFilter builder.Cond
|
||||
for _, eID := range entityIDs {
|
||||
if entitiesFilter == nil {
|
||||
entitiesFilter = getSubscriberCondForEntity(entityType, eID)
|
||||
continue
|
||||
}
|
||||
entitiesFilter = entitiesFilter.Or(getSubscriberCondForEntity(entityType, eID))
|
||||
}
|
||||
|
||||
var subscriptions []*Subscription
|
||||
err = s.
|
||||
Where("user_id = ?", u.ID).
|
||||
And(cond).
|
||||
Get(subscription)
|
||||
if !exists {
|
||||
And(entitiesFilter).
|
||||
Find(&subscriptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscription.Entity = subscription.EntityType.String()
|
||||
|
||||
return subscription, err
|
||||
listsToSubscriptions = make(map[int64]*Subscription)
|
||||
for _, sub := range subscriptions {
|
||||
sub.Entity = sub.EntityType.String()
|
||||
listsToSubscriptions[sub.EntityID] = sub
|
||||
}
|
||||
return listsToSubscriptions, nil
|
||||
}
|
||||
|
||||
func getSubscribersForEntity(s *xorm.Session, entityType SubscriptionEntityType, entityID int64) (subscriptions []*Subscription, err error) {
|
||||
|
||||
@@ -179,7 +179,16 @@ func (la *TaskAssginee) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
err = updateListByTaskID(s, la.TaskID)
|
||||
return
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
return events.Dispatch(&TaskAssigneeDeletedEvent{
|
||||
Task: &Task{ID: la.TaskID},
|
||||
Assignee: &user.User{ID: la.UserID},
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
// Create adds a new assignee to a task
|
||||
@@ -221,6 +230,19 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, list *Li
|
||||
return ErrUserDoesNotHaveAccessToList{list.ID, newAssigneeID}
|
||||
}
|
||||
|
||||
exist, err := s.
|
||||
Where("task_id = ? AND user_id = ?", t.ID, newAssigneeID).
|
||||
Exist(&TaskAssginee{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
return &ErrUserAlreadyAssigned{
|
||||
UserID: newAssigneeID,
|
||||
TaskID: t.ID,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.Insert(TaskAssginee{
|
||||
TaskID: t.ID,
|
||||
UserID: newAssigneeID,
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
@@ -44,7 +46,7 @@ type TaskAttachment struct {
|
||||
}
|
||||
|
||||
// TableName returns the table name for task attachments
|
||||
func (TaskAttachment) TableName() string {
|
||||
func (*TaskAttachment) TableName() string {
|
||||
return "task_attachments"
|
||||
}
|
||||
|
||||
@@ -84,7 +86,11 @@ func (ta *TaskAttachment) NewAttachment(s *xorm.Session, f io.ReadCloser, realna
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return events.Dispatch(&TaskAttachmentCreatedEvent{
|
||||
Task: &Task{ID: ta.TaskID},
|
||||
Attachment: ta,
|
||||
Doer: ta.CreatedBy,
|
||||
})
|
||||
}
|
||||
|
||||
// ReadOne returns a task attachment
|
||||
@@ -209,7 +215,16 @@ func (ta *TaskAttachment) Delete(s *xorm.Session, a web.Auth) error {
|
||||
if err != nil && files.IsErrFileDoesNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
return events.Dispatch(&TaskAttachmentDeletedEvent{
|
||||
Task: &Task{ID: ta.TaskID},
|
||||
Attachment: ta,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
func getTaskAttachmentsByTaskIDs(s *xorm.Session, taskIDs []int64) (attachments []*TaskAttachment, err error) {
|
||||
|
||||
@@ -74,7 +74,8 @@ func validateTaskField(fieldName string) error {
|
||||
taskPropertyUpdated,
|
||||
taskPropertyPosition,
|
||||
taskPropertyKanbanPosition,
|
||||
taskPropertyBucketID:
|
||||
taskPropertyBucketID,
|
||||
taskPropertyIndex:
|
||||
return nil
|
||||
}
|
||||
return ErrInvalidTaskField{TaskField: fieldName}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/vectordotdev/go-datemath"
|
||||
"xorm.io/xorm/schemas"
|
||||
@@ -44,10 +45,47 @@ const (
|
||||
taskFilterComparatorIn taskFilterComparator = "in"
|
||||
)
|
||||
|
||||
// Guess what you get back if you ask Safari for a rfc 3339 formatted date?
|
||||
const safariDateAndTime = "2006-01-02 15:04"
|
||||
const safariDate = "2006-01-02"
|
||||
|
||||
type taskFilter struct {
|
||||
field string
|
||||
value interface{} // Needs to be an interface to be able to hold the field's native value
|
||||
comparator taskFilterComparator
|
||||
isNumeric bool
|
||||
}
|
||||
|
||||
func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
|
||||
value, err = time.Parse(time.RFC3339, timeString)
|
||||
if err != nil {
|
||||
value, err = time.Parse(safariDateAndTime, timeString)
|
||||
}
|
||||
if err != nil {
|
||||
value, err = time.Parse(safariDate, timeString)
|
||||
}
|
||||
if err != nil {
|
||||
// Here we assume a date like 2022-11-1 and try to parse it manually
|
||||
parts := strings.Split(timeString, "-")
|
||||
if len(parts) < 3 {
|
||||
return
|
||||
}
|
||||
year, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
month, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
day, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
value = time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
|
||||
return value.In(config.GetTimeZone()), nil
|
||||
}
|
||||
return value.In(config.GetTimeZone()), err
|
||||
}
|
||||
|
||||
func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
|
||||
@@ -90,8 +128,9 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
|
||||
}
|
||||
|
||||
// Cast the field value to its native type
|
||||
var reflectValue *reflect.StructField
|
||||
if len(c.FilterValue) > i {
|
||||
filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i])
|
||||
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i])
|
||||
if err != nil {
|
||||
return nil, ErrInvalidTaskFilterValue{
|
||||
Value: filter.field,
|
||||
@@ -99,6 +138,9 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if reflectValue != nil {
|
||||
filter.isNumeric = reflectValue.Type.Kind() == reflect.Int64
|
||||
}
|
||||
|
||||
filters = append(filters, filter)
|
||||
}
|
||||
@@ -165,8 +207,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
|
||||
if err == nil {
|
||||
value = t.Time(datemath.WithLocation(config.GetTimeZone()))
|
||||
} else {
|
||||
value, err = time.Parse(time.RFC3339, rawValue)
|
||||
value = value.(time.Time).In(config.GetTimeZone())
|
||||
value, err = parseTimeFromUserInput(rawValue)
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
@@ -192,7 +233,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
|
||||
return
|
||||
}
|
||||
|
||||
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (nativeValue interface{}, err error) {
|
||||
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (reflectField *reflect.StructField, nativeValue interface{}, err error) {
|
||||
|
||||
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
|
||||
|
||||
@@ -203,11 +244,11 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
||||
for _, val := range vals {
|
||||
v, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
valueSlice = append(valueSlice, v)
|
||||
}
|
||||
return valueSlice, nil
|
||||
return nil, valueSlice, nil
|
||||
}
|
||||
|
||||
nativeValue, err = strconv.ParseInt(value, 10, 64)
|
||||
@@ -217,12 +258,12 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
||||
if realFieldName == "Assignees" {
|
||||
vals := strings.Split(value, ",")
|
||||
valueSlice := append([]string{}, vals...)
|
||||
return valueSlice, nil
|
||||
return nil, valueSlice, nil
|
||||
}
|
||||
|
||||
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTaskField{TaskField: fieldName}
|
||||
return nil, nil, ErrInvalidTaskField{TaskField: fieldName}
|
||||
}
|
||||
|
||||
if comparator == taskFilterComparatorIn {
|
||||
@@ -231,12 +272,13 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
||||
for _, val := range vals {
|
||||
v, err := getValueForField(field, val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
valueSlice = append(valueSlice, v)
|
||||
}
|
||||
return valueSlice, nil
|
||||
return nil, valueSlice, nil
|
||||
}
|
||||
|
||||
return getValueForField(field, value)
|
||||
val, err := getValueForField(field, value)
|
||||
return &field, val, err
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ const (
|
||||
taskPropertyPosition string = "position"
|
||||
taskPropertyKanbanPosition string = "kanban_position"
|
||||
taskPropertyBucketID string = "bucket_id"
|
||||
taskPropertyIndex string = "index"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -109,7 +109,16 @@ func (tc *TaskComment) Delete(s *xorm.Session, a web.Auth) error {
|
||||
if deleted == 0 {
|
||||
return ErrTaskCommentDoesNotExist{ID: tc.ID}
|
||||
}
|
||||
return err
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&TaskCommentDeletedEvent{
|
||||
Task: &Task{ID: tc.TaskID},
|
||||
Comment: tc,
|
||||
Doer: tc.Author,
|
||||
})
|
||||
}
|
||||
|
||||
// Update updates a task text by its ID
|
||||
|
||||
@@ -37,7 +37,9 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
||||
|
||||
var tasks []*Task
|
||||
err = s.
|
||||
Where("due_date is not null and due_date < ?", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||
Where("due_date is not null AND due_date < ? AND lists.is_archived = false AND namespaces.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||
Join("LEFT", "lists", "lists.id = tasks.list_id").
|
||||
Join("LEFT", "namespaces", "lists.namespace_id = namespaces.id").
|
||||
And("done = false").
|
||||
Find(&tasks)
|
||||
if err != nil {
|
||||
@@ -92,10 +94,10 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
||||
if !exists {
|
||||
uts[t.User.ID] = &userWithTasks{
|
||||
user: t.User,
|
||||
tasks: []*Task{},
|
||||
tasks: make(map[int64]*Task),
|
||||
}
|
||||
}
|
||||
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
||||
uts[t.User.ID].tasks[t.Task.ID] = t.Task
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +106,7 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
||||
|
||||
type userWithTasks struct {
|
||||
user *user.User
|
||||
tasks []*Task
|
||||
tasks map[int64]*Task
|
||||
}
|
||||
|
||||
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
|
||||
@@ -138,9 +140,13 @@ func RegisterOverdueReminderCron() {
|
||||
}
|
||||
|
||||
if len(ut.tasks) == 1 {
|
||||
n = &UndoneTaskOverdueNotification{
|
||||
User: ut.user,
|
||||
Task: ut.tasks[0],
|
||||
// We know there's only one entry in the map so this is actually O(1) and we can use it to get the
|
||||
// first entry without knowing the key of it.
|
||||
for _, t := range ut.tasks {
|
||||
n = &UndoneTaskOverdueNotification{
|
||||
User: ut.user,
|
||||
Task: t,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ package models
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
|
||||
@@ -98,7 +100,7 @@ type TaskRelation struct {
|
||||
}
|
||||
|
||||
// TableName holds the table name for the task relation table
|
||||
func (TaskRelation) TableName() string {
|
||||
func (*TaskRelation) TableName() string {
|
||||
return "task_relations"
|
||||
}
|
||||
|
||||
@@ -190,7 +192,16 @@ func (rel *TaskRelation) Create(s *xorm.Session, a web.Auth) error {
|
||||
rel,
|
||||
otherRelation,
|
||||
})
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
return events.Dispatch(&TaskRelationCreatedEvent{
|
||||
Task: &Task{ID: rel.TaskID},
|
||||
Relation: rel,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete removes a task relation
|
||||
@@ -202,7 +213,7 @@ func (rel *TaskRelation) Create(s *xorm.Session, a web.Auth) error {
|
||||
// @Param relation body models.TaskRelation true "The relation object"
|
||||
// @Param taskID path int true "Task ID"
|
||||
// @Param relationKind path string true "The kind of the relation. See the TaskRelation type for more info."
|
||||
// @Param otherTaskID path int true "The id of the other task."
|
||||
// @Param otherTaskId path int true "The id of the other task."
|
||||
// @Success 200 {object} models.Message "The task relation was successfully deleted."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid task relation object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The task relation was not found."
|
||||
@@ -241,5 +252,14 @@ func (rel *TaskRelation) Delete(s *xorm.Session, a web.Auth) error {
|
||||
_, err = s.
|
||||
Where(cond).
|
||||
Delete(&TaskRelation{})
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
return events.Dispatch(&TaskRelationDeletedEvent{
|
||||
Task: &Task{ID: rel.TaskID},
|
||||
Relation: rel,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
+197
-40
@@ -98,6 +98,9 @@ type Task struct {
|
||||
// All attachments this task has
|
||||
Attachments []*TaskAttachment `xorm:"-" json:"attachments"`
|
||||
|
||||
// If this task has a cover image, the field will return the id of the attachment that is the cover image.
|
||||
CoverImageAttachmentID int64 `xorm:"bigint default 0" json:"cover_image_attachment_id"`
|
||||
|
||||
// True if a task is a favorite task. Favorite tasks show up in a separate "Important" list. This value depends on the user making the call to the api.
|
||||
IsFavorite bool `xorm:"-" json:"is_favorite"`
|
||||
|
||||
@@ -137,7 +140,7 @@ type TaskWithComments struct {
|
||||
}
|
||||
|
||||
// TableName returns the table name for listtasks
|
||||
func (Task) TableName() string {
|
||||
func (*Task) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
@@ -224,6 +227,9 @@ func getFilterCond(f *taskFilter, includeNulls bool) (cond builder.Cond, err err
|
||||
|
||||
if includeNulls {
|
||||
cond = builder.Or(cond, &builder.IsNull{field})
|
||||
if f.isNumeric {
|
||||
cond = builder.Or(cond, &builder.IsNull{field}, &builder.Eq{field: 0})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
@@ -303,10 +309,10 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
||||
// Because it does not have support for NULLS FIRST or NULLS LAST we work around this by
|
||||
// first sorting for null (or not null) values and then the order we actually want to.
|
||||
if db.Type() == schemas.MYSQL {
|
||||
orderby += param.sortBy + " IS NULL, "
|
||||
orderby += "`" + param.sortBy + "` IS NULL, "
|
||||
}
|
||||
|
||||
orderby += param.sortBy + " " + param.orderBy.String()
|
||||
orderby += "`" + param.sortBy + "` " + param.orderBy.String()
|
||||
|
||||
// Postgres and sqlite allow us to control how columns with null values are sorted.
|
||||
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
|
||||
@@ -577,7 +583,9 @@ func GetTasksByUIDs(s *xorm.Session, uids []string, a web.Auth) (tasks []*Task,
|
||||
|
||||
func getRemindersForTasks(s *xorm.Session, taskIDs []int64) (reminders []*TaskReminder, err error) {
|
||||
reminders = []*TaskReminder{}
|
||||
err = s.In("task_id", taskIDs).Find(&reminders)
|
||||
err = s.In("task_id", taskIDs).
|
||||
OrderBy("reminder asc").
|
||||
Find(&reminders)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -604,7 +612,7 @@ func addAssigneesToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Ta
|
||||
|
||||
// Get all labels for all the tasks
|
||||
func addLabelsToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task) (err error) {
|
||||
labels, _, _, err := getLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
labels, _, _, err := GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
|
||||
TaskIDs: taskIDs,
|
||||
Page: -1,
|
||||
})
|
||||
@@ -809,23 +817,28 @@ func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucketLimit bool) (targetBucket *Bucket, 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
|
||||
return nil, err
|
||||
}
|
||||
if bucket != nil {
|
||||
task.BucketID = bucket.ID
|
||||
}
|
||||
}
|
||||
|
||||
if task.BucketID == 0 && originalTask != nil && originalTask.BucketID != 0 {
|
||||
task.BucketID = originalTask.BucketID
|
||||
}
|
||||
|
||||
// Either no bucket was provided or the task was moved between lists
|
||||
if task.BucketID == 0 || (originalTask != nil && task.ListID != 0 && originalTask.ListID != task.ListID) {
|
||||
bucket, err = getDefaultBucket(s, task.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
task.BucketID = bucket.ID
|
||||
}
|
||||
@@ -833,7 +846,7 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
|
||||
if bucket == nil {
|
||||
bucket, err = getBucketByID(s, task.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,7 +860,7 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
|
||||
// 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
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,7 +868,7 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
|
||||
task.Done = true
|
||||
}
|
||||
|
||||
return nil
|
||||
return bucket, nil
|
||||
}
|
||||
|
||||
func calculateDefaultPosition(entityID int64, position float64) float64 {
|
||||
@@ -866,6 +879,19 @@ func calculateDefaultPosition(entityID int64, position float64) float64 {
|
||||
return position
|
||||
}
|
||||
|
||||
func getNextTaskIndex(s *xorm.Session, listID int64) (nextIndex int64, err error) {
|
||||
latestTask := &Task{}
|
||||
_, err = s.
|
||||
Where("list_id = ?", listID).
|
||||
OrderBy("`index` desc").
|
||||
Get(latestTask)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return latestTask.Index + 1, nil
|
||||
}
|
||||
|
||||
// Create is the implementation to create a list task
|
||||
// @Summary Create a task
|
||||
// @Description Inserts a task into a list.
|
||||
@@ -911,22 +937,20 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
||||
}
|
||||
|
||||
// Get the default bucket and move the task there
|
||||
err = setTaskBucket(s, t, nil, true)
|
||||
_, err = setTaskBucket(s, t, nil, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the index for this task
|
||||
latestTask := &Task{}
|
||||
_, err = s.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask)
|
||||
t.Index, err = getNextTaskIndex(s, t.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Index = latestTask.Index + 1
|
||||
// If no position was supplied, set a default one
|
||||
t.Position = calculateDefaultPosition(latestTask.ID+1, t.Position)
|
||||
t.KanbanPosition = calculateDefaultPosition(latestTask.ID+1, t.KanbanPosition)
|
||||
t.Position = calculateDefaultPosition(t.Index, t.Position)
|
||||
t.KanbanPosition = calculateDefaultPosition(t.Index, t.KanbanPosition)
|
||||
if _, err = s.Insert(t); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -979,6 +1003,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the task (aka its list)"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{id} [post]
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
@@ -1003,13 +1028,21 @@ 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)
|
||||
|
||||
if err := setTaskBucket(s, t, &ot, t.BucketID != ot.BucketID); err != nil {
|
||||
targetBucket, err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the task was moved into the done bucket and the task has a repeating cycle we should not update
|
||||
// the bucket.
|
||||
if targetBucket.IsDoneBucket && t.RepeatAfter > 0 {
|
||||
t.Done = true // This will trigger the correct re-scheduling of the task (happening in updateDone later)
|
||||
t.BucketID = ot.BucketID
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -1038,20 +1071,35 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
"position",
|
||||
"repeat_mode",
|
||||
"kanban_position",
|
||||
"cover_image_attachment_id",
|
||||
}
|
||||
|
||||
// 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{}
|
||||
_, err = s.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask)
|
||||
t.Index, err = getNextTaskIndex(s, t.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Index = latestTask.Index + 1
|
||||
colsToUpdate = append(colsToUpdate, "index")
|
||||
}
|
||||
|
||||
// If a task attachment is being set as cover image, check if the attachment actually belongs to the task
|
||||
if t.CoverImageAttachmentID != 0 {
|
||||
is, err := s.Exist(&TaskAttachment{
|
||||
TaskID: t.ID,
|
||||
ID: t.CoverImageAttachmentID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !is {
|
||||
return &ErrAttachmentDoesNotBelongToTask{
|
||||
AttachmentID: t.CoverImageAttachmentID,
|
||||
TaskID: t.ID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wasFavorite, err := isFavorite(s, t.ID, a, FavoriteKindTask)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -1147,6 +1195,10 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
if !t.IsFavorite {
|
||||
ot.IsFavorite = false
|
||||
}
|
||||
// Attachment cover image
|
||||
if t.CoverImageAttachmentID == 0 {
|
||||
ot.CoverImageAttachmentID = 0
|
||||
}
|
||||
|
||||
_, err = s.ID(t.ID).
|
||||
Cols(colsToUpdate...).
|
||||
@@ -1155,6 +1207,21 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update all positions if the newly saved position is < 0.1
|
||||
if ot.Position < 0.1 {
|
||||
err = recalculateTaskPositions(s, t.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if ot.KanbanPosition < 0.1 {
|
||||
err = recalculateTaskKanbanPositions(s, t.BucketID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Get the task updated timestamp in a new struct - if we'd just try to put it into t which we already have, it
|
||||
// would still contain the old updated date.
|
||||
nt := &Task{}
|
||||
@@ -1163,6 +1230,8 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
t.Updated = nt.Updated
|
||||
t.Position = nt.Position
|
||||
t.KanbanPosition = nt.KanbanPosition
|
||||
|
||||
doer, _ := user.GetFromAuth(a)
|
||||
err = events.Dispatch(&TaskUpdatedEvent{
|
||||
@@ -1176,6 +1245,62 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
return updateListLastUpdated(s, &List{ID: t.ListID})
|
||||
}
|
||||
|
||||
func recalculateTaskKanbanPositions(s *xorm.Session, bucketID int64) (err error) {
|
||||
|
||||
allTasks := []*Task{}
|
||||
err = s.
|
||||
Where("bucket_id = ?", bucketID).
|
||||
OrderBy("kanban_position asc").
|
||||
Find(&allTasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
maxPosition := math.Pow(2, 32)
|
||||
|
||||
for i, task := range allTasks {
|
||||
|
||||
currentPosition := maxPosition / float64(len(allTasks)) * (float64(i + 1))
|
||||
|
||||
_, err = s.Cols("kanban_position").
|
||||
Where("id = ?", task.ID).
|
||||
Update(&Task{KanbanPosition: currentPosition})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func recalculateTaskPositions(s *xorm.Session, listID int64) (err error) {
|
||||
|
||||
allTasks := []*Task{}
|
||||
err = s.
|
||||
Where("list_id = ?", listID).
|
||||
OrderBy("position asc").
|
||||
Find(&allTasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
maxPosition := math.Pow(2, 32)
|
||||
|
||||
for i, task := range allTasks {
|
||||
|
||||
currentPosition := maxPosition / float64(len(allTasks)) * (float64(i + 1))
|
||||
|
||||
_, err = s.Cols("position").
|
||||
Where("id = ?", task.ID).
|
||||
Update(&Task{Position: currentPosition})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func addOneMonthToDate(d time.Time) time.Time {
|
||||
return time.Date(d.Year(), d.Month()+1, d.Day(), d.Hour(), d.Minute(), d.Second(), d.Nanosecond(), config.GetTimeZone())
|
||||
}
|
||||
@@ -1289,18 +1414,39 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
|
||||
}
|
||||
}
|
||||
|
||||
// If a task has a start and end date, the end date should keep the difference to the start date when setting them as new
|
||||
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
|
||||
diff := oldTask.EndDate.Sub(oldTask.StartDate)
|
||||
newTask.StartDate = now.Add(repeatDuration)
|
||||
newTask.EndDate = now.Add(repeatDuration + diff)
|
||||
} else {
|
||||
if !oldTask.StartDate.IsZero() {
|
||||
// We want to preserve intervals among the due, start and end dates.
|
||||
// The due date is used as a reference point for all new dates, so the
|
||||
// behaviour depends on whether the due date is set at all.
|
||||
if oldTask.DueDate.IsZero() {
|
||||
// If a task has no due date, but does have a start and end date, the
|
||||
// end date should keep the difference to the start date when setting
|
||||
// them as new
|
||||
if !oldTask.StartDate.IsZero() && !oldTask.EndDate.IsZero() {
|
||||
diff := oldTask.EndDate.Sub(oldTask.StartDate)
|
||||
newTask.StartDate = now.Add(repeatDuration)
|
||||
newTask.EndDate = now.Add(repeatDuration + diff)
|
||||
} else {
|
||||
if !oldTask.StartDate.IsZero() {
|
||||
newTask.StartDate = now.Add(repeatDuration)
|
||||
}
|
||||
|
||||
if !oldTask.EndDate.IsZero() {
|
||||
newTask.EndDate = now.Add(repeatDuration)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the old task has a start and due date, we set the new start date
|
||||
// to preserve the interval between them.
|
||||
if !oldTask.StartDate.IsZero() {
|
||||
diff := oldTask.DueDate.Sub(oldTask.StartDate)
|
||||
newTask.StartDate = newTask.DueDate.Add(-diff)
|
||||
}
|
||||
|
||||
// If the old task has an end and due date, we set the new end date
|
||||
// to preserve the interval between them.
|
||||
if !oldTask.EndDate.IsZero() {
|
||||
newTask.EndDate = now.Add(repeatDuration)
|
||||
diff := oldTask.DueDate.Sub(oldTask.EndDate)
|
||||
newTask.EndDate = newTask.DueDate.Add(-diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1309,9 +1455,9 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) {
|
||||
|
||||
// This helper function updates the reminders, doneAt, start and end dates of the *old* task
|
||||
// and saves the new values in the newTask object.
|
||||
// We make a few assumtions here:
|
||||
// 1. Everything in oldTask is the truth - we figure out if we update anything at all if oldTask.RepeatAfter has a value > 0
|
||||
// 2. Because of 1., this functions should not be used to update values other than Done in the same go
|
||||
// We make a few assumptions here:
|
||||
// 1. Everything in oldTask is the truth - we figure out if we update anything at all if oldTask.RepeatAfter has a value > 0
|
||||
// 2. Because of 1., this functions should not be used to update values other than Done in the same go
|
||||
func updateDone(oldTask *Task, newTask *Task) {
|
||||
if !oldTask.Done && newTask.Done {
|
||||
switch oldTask.RepeatMode {
|
||||
@@ -1345,8 +1491,14 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve duplicates and sort them
|
||||
reminderMap := make(map[int64]time.Time, len(reminders))
|
||||
for _, reminder := range reminders {
|
||||
reminderMap[reminder.UTC().Unix()] = reminder
|
||||
}
|
||||
|
||||
// Loop through all reminders and add them
|
||||
for _, r := range reminders {
|
||||
for _, r := range reminderMap {
|
||||
_, err = s.Insert(&TaskReminder{TaskID: t.ID, Reminder: r})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1362,6 +1514,11 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro
|
||||
return
|
||||
}
|
||||
|
||||
func updateTaskLastUpdated(s *xorm.Session, task *Task) error {
|
||||
_, err := s.ID(task.ID).Cols("updated").Update(task)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete implements the delete method for listTask
|
||||
// @Summary Delete a task
|
||||
// @Description Deletes a task from a list. This does not mean "mark it done".
|
||||
@@ -1455,12 +1612,12 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
// @Router /tasks/{ID} [get]
|
||||
func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
taskMap := make(map[int64]*Task, 1)
|
||||
taskMap[t.ID] = &Task{}
|
||||
*taskMap[t.ID], err = GetTaskByIDSimple(s, t.ID)
|
||||
*t, err = GetTaskByIDSimple(s, t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
taskMap := make(map[int64]*Task, 1)
|
||||
taskMap[t.ID] = t
|
||||
|
||||
err = addMoreInfoToTasks(s, taskMap, a)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,11 +20,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestTask_Create(t *testing.T) {
|
||||
@@ -243,6 +244,33 @@ func TestTask_Update(t *testing.T) {
|
||||
"bucket_id": 3,
|
||||
}, false)
|
||||
})
|
||||
t.Run("moving a repeating task to the done bucket", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 28,
|
||||
Title: "test updated",
|
||||
ListID: 1,
|
||||
BucketID: 3, // Bucket 3 is the done bucket
|
||||
RepeatAfter: 3600,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, task.Done)
|
||||
assert.Equal(t, int64(1), task.BucketID) // Bucket should not be updated
|
||||
|
||||
db.AssertExists(t, "tasks", map[string]interface{}{
|
||||
"id": 28,
|
||||
"done": false,
|
||||
"title": "test updated",
|
||||
"list_id": 1,
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("default bucket when moving a task between lists", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
@@ -308,8 +336,9 @@ func TestTask_Update(t *testing.T) {
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 28,
|
||||
Done: true,
|
||||
ID: 28,
|
||||
Done: true,
|
||||
RepeatAfter: 3600,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
@@ -324,6 +353,42 @@ func TestTask_Update(t *testing.T) {
|
||||
"bucket_id": 1,
|
||||
}, false)
|
||||
})
|
||||
t.Run("moving a task between lists should give it a correct index", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 12,
|
||||
ListID: 2, // From list 1
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), task.Index)
|
||||
})
|
||||
t.Run("the same date multiple times should be saved once", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Title: "test",
|
||||
Reminders: []time.Time{
|
||||
time.Unix(1674745156, 0),
|
||||
time.Unix(1674745156, 223),
|
||||
},
|
||||
ListID: 1,
|
||||
}
|
||||
err := task.Update(s, u)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
|
||||
db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTask_Delete(t *testing.T) {
|
||||
|
||||
@@ -79,7 +79,7 @@ func (tm *TeamMember) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Delete deletes a user from a team
|
||||
// @Summary Remove a user from a team
|
||||
// @Description Remove a user from a team. This will also revoke any access this user might have via that team.
|
||||
// @Description Remove a user from a team. This will also revoke any access this user might have via that team. A user can remove themselves from the team if they are not the last user in the team.
|
||||
// @tags team
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
@@ -28,6 +29,13 @@ func (tm *TeamMember) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
|
||||
// CanDelete checks if the user can delete a new team member
|
||||
func (tm *TeamMember) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
u, err := user.GetUserByUsername(s, tm.Username)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if u.ID == a.GetID() {
|
||||
return true, nil
|
||||
}
|
||||
return tm.IsAdmin(s, a)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ type TeamMember struct {
|
||||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamMember) TableName() string {
|
||||
func (*TeamMember) TableName() string {
|
||||
return "team_members"
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/web/handler"
|
||||
|
||||
@@ -51,6 +49,7 @@ type Provider struct {
|
||||
Key string `json:"key"`
|
||||
OriginalAuthURL string `json:"-"`
|
||||
AuthURL string `json:"auth_url"`
|
||||
LogoutURL string `json:"logout_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"-"`
|
||||
openIDProvider *oidc.Provider
|
||||
@@ -65,7 +64,7 @@ type claims struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
petname.NonDeterministicMode()
|
||||
}
|
||||
|
||||
func (p *Provider) setOicdProvider() (err error) {
|
||||
@@ -266,7 +265,7 @@ func getOrCreateUser(s *xorm.Session, cl *claims, issuer, subject string) (u *us
|
||||
Name: u.Name,
|
||||
Issuer: issuer,
|
||||
Subject: subject,
|
||||
})
|
||||
}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ func GetAllProviders() (providers []*Provider, err error) {
|
||||
}
|
||||
|
||||
provider, err := getProviderFromMap(pi)
|
||||
|
||||
if err != nil {
|
||||
if provider != nil {
|
||||
log.Errorf("Error while getting openid provider %s: %s", provider.Name, err)
|
||||
@@ -119,12 +120,18 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
|
||||
|
||||
k := getKeyFromName(name)
|
||||
|
||||
logoutURL, ok := pi["logouturl"].(string)
|
||||
if !ok {
|
||||
logoutURL = ""
|
||||
}
|
||||
|
||||
provider = &Provider{
|
||||
Name: pi["name"].(string),
|
||||
Key: k,
|
||||
AuthURL: pi["authurl"].(string),
|
||||
OriginalAuthURL: pi["authurl"].(string),
|
||||
ClientSecret: pi["clientsecret"].(string),
|
||||
LogoutURL: logoutURL,
|
||||
}
|
||||
|
||||
cl, is := pi["clientid"].(int)
|
||||
@@ -143,7 +150,6 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
|
||||
ClientID: provider.ClientID,
|
||||
ClientSecret: provider.ClientSecret,
|
||||
RedirectURL: config.AuthOpenIDRedirectURL.GetString() + k,
|
||||
|
||||
// Discovery returns the OAuth2 endpoints.
|
||||
Endpoint: provider.openIDProvider.Endpoint(),
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ package gravatar
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -74,7 +74,7 @@ func (g *Provider) GetAvatar(user *user.User, size int64) ([]byte, string, error
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
avatarContent, err := ioutil.ReadAll(resp.Body)
|
||||
avatarContent, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -20,13 +20,14 @@ import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
)
|
||||
|
||||
@@ -82,7 +83,7 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
avatar, err = ioutil.ReadAll(buf)
|
||||
avatar, err = io.ReadAll(buf)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -295,6 +295,11 @@ func GetListBackground(c echo.Context) error {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
stat, err := bgFile.File.Stat()
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Unsplash requires pingbacks as per their api usage guidelines.
|
||||
// To do this in a privacy-preserving manner, we do the ping from inside of Vikunja to not expose any user details.
|
||||
@@ -306,6 +311,11 @@ func GetListBackground(c echo.Context) error {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Set Last-Modified header if we have the file stat, so clients can decide whether to use cached files
|
||||
if stat != nil {
|
||||
c.Response().Header().Set(echo.HeaderLastModified, stat.ModTime().UTC().Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
return c.Stream(http.StatusOK, "image/jpg", bgFile.File)
|
||||
}
|
||||
@@ -331,6 +341,11 @@ func RemoveListBackground(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = list.DeleteBackgroundFileIfExists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
list.BackgroundFileID = 0
|
||||
list.BackgroundInformation = nil
|
||||
list.BackgroundBlurHash = ""
|
||||
|
||||
@@ -47,7 +47,7 @@ func unsplashImage(url string, c echo.Context) error {
|
||||
// @Description Get an unsplash image. **Returns json on error.**
|
||||
// @tags list
|
||||
// @Produce octet-stream
|
||||
// @Param thumb path int true "Unsplash Image ID"
|
||||
// @Param image path int true "Unsplash Image ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {} string "The image"
|
||||
// @Failure 404 {object} models.Message "The image does not exist."
|
||||
@@ -67,7 +67,7 @@ func ProxyUnsplashImage(c echo.Context) error {
|
||||
// @Description Get an unsplash thumbnail image. The thumbnail is cropped to a max width of 200px. **Returns json on error.**
|
||||
// @tags list
|
||||
// @Produce octet-stream
|
||||
// @Param thumb path int true "Unsplash Image ID"
|
||||
// @Param image path int true "Unsplash Image ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {} string "The thumbnail"
|
||||
// @Failure 404 {object} models.Message "The image does not exist."
|
||||
|
||||
@@ -54,11 +54,9 @@ func (p *Provider) Search(s *xorm.Session, search string, page int64) (result []
|
||||
// @Router /lists/{id}/backgrounds/upload [put]
|
||||
func (p *Provider) Set(s *xorm.Session, img *background.Image, list *models.List, auth web.Auth) (err error) {
|
||||
// Remove the old background if one exists
|
||||
if list.BackgroundFileID != 0 {
|
||||
file := files.File{ID: list.BackgroundFileID}
|
||||
if err := file.Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
err = list.DeleteBackgroundFileIfExists()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file := &files.File{}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user