forked from vikunja/vikunja
Compare commits
457 Commits
Author | SHA1 | Date |
---|---|---|
kolaente | 556abcd9d2 | |
kolaente | b10dbce1a1 | |
renovate | 7b77974b03 | |
renovate | 05358350af | |
kolaente | 9fc08a0790 | |
renovate | f6b897e8e7 | |
renovate | 8de78c48f8 | |
renovate | a13126d1dd | |
renovate | b96e681270 | |
kolaente | f5fd849a0b | |
renovate | 144e115394 | |
renovate | 815fc10135 | |
renovate | 955a1771ae | |
renovate | aca930655b | |
renovate | 2ba78d240f | |
renovate | cad18945bb | |
renovate | 9fc0fc184d | |
renovate | 1577c8d3f3 | |
renovate | 2eb4d07aa9 | |
renovate | 4789a69455 | |
renovate | 35f01a4549 | |
renovate | dca51c762b | |
kolaente | 6515dd6908 | |
renovate | 0ea4de3f56 | |
renovate | c2104a3374 | |
renovate | aaceb4e968 | |
renovate | 4ec4c0a65d | |
renovate | c68bd235e8 | |
konrad | df2e36c2a3 | |
kolaente | f5a33478f2 | |
kolaente | 0d044997df | |
kolaente | 5e40f4ec89 | |
kolaente | 5871d32c2d | |
kolaente | 3af9855148 | |
kolaente | e5394d6d4b | |
kolaente | b8769c746c | |
renovate | b331fdd29a | |
kolaente | 2fc690a783 | |
renovate | bcb286a7f0 | |
kolaente | 008908eb49 | |
renovate | 12e0e12bae | |
konrad | d43762e9d9 | |
kolaente | 631a265d2d | |
kolaente | e113fe34d0 | |
kolaente | 0eb47096db | |
kolaente | 0e1904d50b | |
kolaente | b4b25499f2 | |
renovate | b735ffc4b3 | |
renovate | 95105aaa35 | |
renovate | 66331b1002 | |
renovate | ed6a27da6a | |
kolaente | b5ee39b887 | |
kolaente | 0d8451ab6e | |
renovate | 81f09f7dc0 | |
kolaente | 5a40100ac5 | |
renovate | 0694314e52 | |
renovate | a547a9eb25 | |
renovate | 0fcd03f561 | |
renovate | ffedd02b08 | |
renovate | c84684a425 | |
kolaente | aed560339b | |
kolaente | 0612f4d0e0 | |
kolaente | ce621ee5d6 | |
kolaente | 9c4bb5a244 | |
kolaente | c076f73a87 | |
Dominik Pschenitschni | 36265fcedf | |
konrad | 53419180be | |
renovate | c5bd09702a | |
kolaente | fcb205a842 | |
kolaente | 4323803fd6 | |
kolaente | 903b8ff438 | |
kolaente | b1fd13bbcb | |
kolaente | 878d19beb8 | |
kolaente | 96ed1e33e3 | |
renovate | 374a0f9ce3 | |
kolaente | 580bd5aeaa | |
kolaente | c359d6a97d | |
kolaente | 038702a2a0 | |
kolaente | 6426d40825 | |
kolaente | bbe102dd57 | |
kolaente | 65484bc432 | |
kolaente | 54f6cc7a64 | |
kolaente | f1b2338227 | |
kolaente | 45defebcf4 | |
kolaente | 86ee8273bc | |
Luca Bernstein | 3adfeb3b34 | |
Felix Breidenstein | 9bb8a26706 | |
Luca Bernstein | 54b7f7127c | |
Luca Bernstein | 25609db567 | |
kolaente | 2e3603507c | |
kolaente | 2efc1b5a87 | |
Arie | 090c67138a | |
kolaente | d8f387f796 | |
kolaente | aaeffe925e | |
kolaente | f814dd03eb | |
kolaente | 2369ce5554 | |
kolaente | c19479757a | |
kolaente | 8fddbf43ba | |
kolaente | beb4d07cf9 | |
kolaente | 10ded56f66 | |
kolaente | d709db4e18 | |
kolaente | 0c8bed4054 | |
kolaente | 9ddd7f4889 | |
kolaente | 3047ccfd4a | |
kolaente | 7f28865903 | |
renovate | a273d1ae76 | |
kolaente | c9e044b3ad | |
kolaente | 8bf0f8bb57 | |
kolaente | 3ccc6365a6 | |
renovate | 8d10130d4c | |
kolaente | 51314f269d | |
renovate | 9eefb2bea9 | |
renovate | 2e5c91efdf | |
kolaente | dbb0f54732 | |
renovate | 6e639d9ccb | |
renovate | a9a8bd54ee | |
renovate | d3a655c75b | |
renovate | e0dc3807f6 | |
konrad | 4e7510995c | |
renovate | f8300c9e1b | |
renovate | ef3f07b677 | |
renovate | ea66875310 | |
renovate | 850ac0c601 | |
renovate | 8ebb642d55 | |
kolaente | 2a569488d7 | |
kolaente | 49b3ae82e4 | |
kolaente | b71e6f8049 | |
kolaente | fa82c71f8c | |
kolaente | 8f473481ac | |
kolaente | 51cd2830dd | |
kolaente | 430057a404 | |
kolaente | 7ffe9b625e | |
kolaente | d47edac376 | |
kolaente | aed1ad6d96 | |
kolaente | 84bcdbf937 | |
kolaente | 280ac1164b | |
kolaente | b6d7323cdf | |
kolaente | 59796fd490 | |
kolaente | 26e2d0bdde | |
kolaente | 251b877015 | |
renovate | b460fa8c82 | |
kolaente | 77fafd5dc3 | |
kolaente | 3688bbde20 | |
kolaente | c51ee94ad1 | |
kolaente | 8f27e7e619 | |
kolaente | 382a7884be | |
renovate | cd345b62c2 | |
renovate | dc2285bcc9 | |
kolaente | 1feb62cc45 | |
kolaente | 117f6b38e1 | |
kolaente | dd461746a6 | |
kolaente | 0f555b7ec7 | |
renovate | f93b68819d | |
kolaente | 79b31673e2 | |
kolaente | f8cc67d37f | |
renovate | 6c92859f8c | |
renovate | ef6fe9500e | |
renovate | 8578f3a927 | |
renovate | bfcebc63b7 | |
renovate | 8cafe84170 | |
renovate | f3319e837a | |
renovate | 7c70b5d4b3 | |
renovate | 1eceecf3ab | |
renovate | 76fa841e9a | |
renovate | 2f601052fd | |
renovate | 8023674adf | |
renovate | 560fa187e0 | |
renovate | a321c3cfb9 | |
kolaente | 6e15d46a93 | |
kolaente | 54348c5891 | |
kolaente | 596d2bf676 | |
kolaente | ac92499b7d | |
kolaente | b1892eaf63 | |
Pavle Portic | b9793a267b | |
kolaente | c906fc2b07 | |
kolaente | 4bb77b5539 | |
kolaente | 5743a4afe5 | |
kolaente | 62325de9cd | |
kolaente | 8759937e3c | |
kolaente | 5cc4927b9e | |
kolaente | 2b074c60a7 | |
kolaente | f5a4c136fb | |
kolaente | 230478aae9 | |
kolaente | 7e99618319 | |
kolaente | 73c4c399e5 | |
kolaente | 25ffa1bc2e | |
kolaente | 4429ba2da1 | |
renovate | db1ccff0de | |
renovate | 2c9ab3d86f | |
kolaente | 951d74b272 | |
kolaente | a38efef734 | |
kolaente | a060cbe820 | |
kolaente | ad17ff5c32 | |
kolaente | d0e09d69d0 | |
kolaente | 7a30294407 | |
kolaente | bc7f6a8586 | |
kolaente | f30a9d1038 | |
kolaente | c62e26b6fe | |
kolaente | f4f8450d16 | |
kolaente | 30e0e98f77 | |
renovate | 12557163b2 | |
renovate | 8b82aab7aa | |
renovate | 70018613da | |
kolaente | 01271c4c01 | |
kolaente | d837f8a624 | |
kolaente | 8869adfc27 | |
renovate | 030bbfa47e | |
kolaente | 7eb3b96a44 | |
kolaente | 2f25b48869 | |
k2s | 172a6214d7 | |
renovate | 92a87cfe4f | |
renovate | 163a4624ee | |
renovate | 37a07aa677 | |
konrad | e52c45d5aa | |
grahammiln | acaa85083f | |
kolaente | f5ebada913 | |
renovate | 7b10176a10 | |
renovate | 3ab0ac9f27 | |
renovate | dc5faaf2cf | |
renovate | 85be5a7bcd | |
renovate | 9ab00fd2e6 | |
kolaente | 9845fcc170 | |
renovate | b4f57dc3e1 | |
tuxthepenguin | 4960a498ff | |
Subhaditya Nath | 96e519ea96 | |
renovate | 81a18661ad | |
renovate | fabd3471a8 | |
renovate | 8cdbc78b1c | |
renovate | 91c89931f8 | |
konrad | e4b50e84a4 | |
kolaente | 726a517bec | |
kolaente | de97fcbd12 | |
kolaente | d3bdafb717 | |
kolaente | e19ad11846 | |
kolaente | 6b51fae093 | |
kolaente | ba2bdff391 | |
kolaente | 7fa0865188 | |
kolaente | 6df865876d | |
kolaente | 2ec7d7a8a8 | |
kolaente | f83b09af59 | |
kolaente | 362706b38d | |
renovate | 1fa1cd365e | |
konrad | 0a1d8c9404 | |
konrad | d08dcc4e44 | |
kolaente | ac6818a476 | |
kolaente | 5cf263a86f | |
renovate | 09c0d14444 | |
renovate | da0bcc6322 | |
renovate | f5b0c603a3 | |
renovate | aa30baf1bc | |
renovate | 581dcd41f1 | |
renovate | d013020268 | |
renovate | bc8a02d794 | |
kolaente | b60c69c5a8 | |
renovate | 9d816205da | |
kolaente | 8aa646e4d9 | |
renovate | b167aac624 | |
kolaente | 75f74b429e | |
renovate | 21541bc118 | |
renovate | 72ee84d43d | |
renovate | f2f6cc68d9 | |
renovate | c870627644 | |
renovate | edc5084278 | |
kolaente | 82158d7718 | |
renovate | 16a98a5c70 | |
renovate | f77f2387b6 | |
renovate | a423e111e9 | |
renovate | f96c2fe23f | |
renovate | 81ab23385f | |
renovate | 325ba622e0 | |
renovate | cda78ea702 | |
kolaente | 8bf2254f4b | |
kolaente | 22e3f242a3 | |
kolaente | 8cb92b3924 | |
kolaente | 44aaf0a4ec | |
renovate | 2bc82bc1a0 | |
kolaente | 43f1daf40c | |
renovate | 545999cf5e | |
renovate | c8aab8a8ad | |
renovate | 7978c91d09 | |
renovate | 519675ab23 | |
renovate | 4e257b82a3 | |
renovate | 386412caca | |
renovate | 8f10a852c2 | |
renovate | d056d2df51 | |
renovate | 4432ebf4a5 | |
renovate | c5611f79ea | |
renovate | d2adf00790 | |
konrad | 1322cb16d7 | |
renovate | 2598550e49 | |
renovate | 9196826390 | |
renovate | 6cbad9546a | |
renovate | df35531e70 | |
renovate | daa7d26b3d | |
renovate | 272fd6dabf | |
kolaente | 1d8d0f140e | |
kolaente | 7af4761cc3 | |
renovate | 7a27e1317f | |
renovate | 59a1e8d0a9 | |
renovate | 4e7ceb22dd | |
renovate | b615f06da3 | |
renovate | 7c6fc41543 | |
renovate | 230c784f55 | |
kolaente | 049ae39c62 | |
shilch | f7a06e4644 | |
kolaente | da318e3db1 | |
renovate | bb88beb417 | |
konrad | 61d49c3a56 | |
renovate | e116fbad79 | |
renovate | f255fb2d5c | |
renovate | 4aa045d710 | |
renovate | b316a412ed | |
renovate | a9296d807f | |
renovate | 614dcaeb9d | |
renovate | 832b7184f3 | |
renovate | 8fb597cae2 | |
renovate | 64cc299fb1 | |
renovate | fbf271978d | |
renovate | 514c11d342 | |
renovate | 7b2fe5db50 | |
Mike | a4c85fed55 | |
renovate | 9d8f9d9de9 | |
renovate | c2e8a8ad98 | |
renovate | 5e4d632e79 | |
renovate | 402c34fb12 | |
renovate | 94fff0ac88 | |
renovate | 62c3dec6e3 | |
renovate | c5d831ec7c | |
renovate | 351d72f02d | |
renovate | 4752888c06 | |
renovate | d31c0dfeac | |
kolaente | a31086a7a9 | |
kolaente | a98119f2d6 | |
kolaente | 8bb3f8d37c | |
kolaente | 190a9f2a4c | |
kolaente | 5c88dfe88e | |
kolaente | 70e005e7ce | |
kolaente | f581885e65 | |
kolaente | 72d3c54efd | |
renovate | 2f0a36eb47 | |
renovate | 22e1a4f151 | |
renovate | a9ff6b7561 | |
renovate | 24e1460e04 | |
konrad | 73ee696fc3 | |
kolaente | 13561f2114 | |
renovate | f9724181b1 | |
renovate | 87515d133d | |
renovate | acc4a8a294 | |
renovate | 9194ac6776 | |
kolaente | 6ece909286 | |
kolaente | 445cc4f79d | |
renovate | d75a13891a | |
renovate | 922eac6029 | |
kolaente | fd0d462bf4 | |
renovate | 86480ad969 | |
kolaente | f8a0a7e953 | |
renovate | b0622732a1 | |
renovate | 072b82412b | |
renovate | ea539cc284 | |
Joseph Kavanagh | 36bf3d216a | |
renovate | efd0970971 | |
renovate | 677ca03489 | |
kolaente | 1fa74cba64 | |
kolaente | 0b7762590f | |
kolaente | c3e0e6405a | |
kolaente | 57e5d10eee | |
kolaente | 88a2cede19 | |
kolaente | 093d0c65ca | |
renovate | da2d5e41c7 | |
kolaente | 9bf32aae99 | |
kolaente | 2aea1691cf | |
kolaente | 4829c89940 | |
kolaente | cf05de19b3 | |
renovate | 2ae1da473e | |
renovate | 62361d8ad9 | |
renovate | 98316a04b2 | |
renovate | ae192c1291 | |
renovate | e3210a11df | |
renovate | 5763aca428 | |
renovate | 609497d08c | |
renovate | b92c370d6d | |
renovate | 0b707369d0 | |
renovate | e64f06fabc | |
renovate | bcfaa78c1a | |
renovate | 29eb0765f3 | |
renovate | 473692cf7a | |
renovate | b9819de157 | |
renovate | b3e520427b | |
renovate | cb74337738 | |
renovate | a03d119a06 | |
kolaente | 2683ef23d5 | |
kolaente | 516c812043 | |
kolaente | 9eca971c93 | |
kolaente | cc612d505f | |
renovate | 53703dd0c4 | |
renovate | 068ad56d2b | |
renovate | a20cbc99fd | |
renovate | eb47161c64 | |
renovate | b194715690 | |
kolaente | 8f55af07c9 | |
renovate | 9869fd694a | |
renovate | ff7270838e | |
renovate | 4bdf147c89 | |
renovate | 1443f8aa2d | |
renovate | af306e5ad3 | |
renovate | 9fbda1d503 | |
renovate | 445703e155 | |
renovate | 4c05912633 | |
renovate | 8dfbb2c344 | |
renovate | 84fe135f69 | |
renovate | 0a6e6dcac1 | |
jayden-c | dcb52c00f1 | |
kolaente | 50b65a517d | |
kolaente | d7e47a28d4 | |
kolaente | c2b6119434 | |
renovate | f46c1c5d13 | |
renovate | 66589ca37d | |
renovate | 45a8c63015 | |
renovate | fce8b89b45 | |
stephen-hill | 82a3330412 | |
ajgon | fb9fa27488 | |
kolaente | cd6cd840a2 | |
kolaente | c2f33868c8 | |
kolaente | 73a99ebd92 | |
kolaente | 68998e90a4 | |
renovate | c67456d420 | |
renovate | 2ab27c72f2 | |
renovate | df5c5e3be2 | |
renovate | 1f067930a2 | |
renovate | 0ca1560bf1 | |
kolaente | 4de8ec56a6 | |
kolaente | ae8db176db | |
renovate | 56efbdc297 | |
renovate | 4764a8e4f0 | |
kolaente | dbd6f36da6 | |
kolaente | 4255bc3a94 | |
kolaente | bb086eb9f8 | |
renovate | c4da0b424b | |
renovate | 037457e865 | |
renovate | e51f125931 | |
renovate | 57d447b165 | |
renovate | 4136255c67 | |
renovate | 0f2013f915 | |
renovate | 0ca31aed28 | |
renovate | 351fe1f624 | |
renovate | abfdae0012 | |
renovate | 5f5936c972 | |
renovate | 111ac92619 | |
kolaente | bf00ea4939 | |
kolaente | 465f6d90ab | |
renovate | 07bda3eae3 | |
kolaente | 03d818fd9f | |
kolaente | f019ae42bb | |
kolaente | 9000f2c3cd | |
kolaente | cc1bb3083f | |
kolaente | be47459c14 | |
renovate | 086f005e3d |
|
@ -0,0 +1,6 @@
|
|||
files/
|
||||
Dockerfile
|
||||
docker-manifest.tmpl
|
||||
docker-manifest-unstable.tmpl
|
||||
*.db
|
||||
*.zip
|
95
.drone.yml
95
.drone.yml
|
@ -62,7 +62,7 @@ services:
|
|||
- name: tmp-mysql-migration
|
||||
path: /var/lib/mysql
|
||||
- name: test-postgres-unit
|
||||
image: postgres:13
|
||||
image: postgres:14
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
|
@ -72,7 +72,7 @@ services:
|
|||
commands:
|
||||
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
|
||||
- name: test-postgres-integration
|
||||
image: postgres:13
|
||||
image: postgres:14
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
|
@ -82,7 +82,7 @@ services:
|
|||
commands:
|
||||
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
|
||||
- name: test-postgres-migration
|
||||
image: postgres:13
|
||||
image: postgres:14
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
|
@ -132,13 +132,15 @@ steps:
|
|||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: lint
|
||||
image: vikunja/golang-build:latest
|
||||
image: golang:1.19-alpine
|
||||
pull: true
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
depends_on: [ build ]
|
||||
commands:
|
||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0
|
||||
- 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.49.0
|
||||
- ./mage-static check:all
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
@ -152,7 +154,7 @@ steps:
|
|||
- unzip vikunja-latest.zip vikunja-unstable-linux-amd64
|
||||
|
||||
- name: test-migration-sqlite
|
||||
image: kolaente/toolbox:latest
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
|
@ -171,7 +173,7 @@ steps:
|
|||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test-migration-mysql
|
||||
image: kolaente/toolbox:latest
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
|
@ -190,7 +192,7 @@ steps:
|
|||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: test-migration-psql
|
||||
image: kolaente/toolbox:latest
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
|
@ -459,7 +461,7 @@ steps:
|
|||
|
||||
# Push the releases to our pseudo-s3-bucket
|
||||
- name: release-latest
|
||||
image: plugins/s3:1
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
|
@ -481,7 +483,7 @@ steps:
|
|||
depends_on: [ sign-release ]
|
||||
|
||||
- name: release-version
|
||||
image: plugins/s3:1
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
|
@ -501,7 +503,7 @@ steps:
|
|||
depends_on: [ sign-release ]
|
||||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm
|
||||
pull: true
|
||||
commands:
|
||||
|
@ -510,11 +512,30 @@ steps:
|
|||
- 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
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on: [ static-build-linux ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm
|
||||
pull: true
|
||||
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: [ static-build-linux ]
|
||||
|
||||
# Push the os releases to our pseudo-s3-bucket
|
||||
- name: release-os-latest
|
||||
image: plugins/s3:1
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
|
@ -533,10 +554,10 @@ steps:
|
|||
- main
|
||||
event:
|
||||
- push
|
||||
depends_on: [ build-os-packages ]
|
||||
depends_on: [ build-os-packages-unstable ]
|
||||
|
||||
- name: release-os-version
|
||||
image: plugins/s3:1
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
|
@ -553,44 +574,7 @@ 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:1
|
||||
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
|
||||
|
@ -621,7 +605,7 @@ steps:
|
|||
- tar -xzf vikunja-theme.tar.gz
|
||||
|
||||
- name: build
|
||||
image: monachus/hugo:v0.75.1
|
||||
image: klakegg/hugo:0.104.2
|
||||
pull: true
|
||||
commands:
|
||||
- cd docs
|
||||
|
@ -662,6 +646,7 @@ steps:
|
|||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
|
||||
- name: docker-arm-unstable
|
||||
image: plugins/docker:linux-arm
|
||||
pull: true
|
||||
|
@ -672,6 +657,7 @@ steps:
|
|||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: unstable-linux-arm
|
||||
dockerfile: Dockerfile.arm32
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
|
@ -688,6 +674,7 @@ steps:
|
|||
repo: vikunja/api
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm
|
||||
dockerfile: Dockerfile.arm32
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
|
@ -874,6 +861,6 @@ steps:
|
|||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 110b782e9b704b4b3b3d618678383718c92262cf3c214f4fe6705d40cd3da367
|
||||
hmac: 5500acb776acae4975592637767035c45af277f1f56c8d10ffd99f38761cd5c8
|
||||
|
||||
...
|
||||
|
|
|
@ -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
|
|
@ -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`)
|
|
@ -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
|
||||
|
|
|
@ -13,10 +13,11 @@ linters:
|
|||
- goheader
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- revive
|
||||
- misspell
|
||||
disable:
|
||||
- scopelint # Obsolete, using exportloopref instead
|
||||
- durationcheck
|
||||
presets:
|
||||
- bugs
|
||||
- unused
|
||||
|
@ -35,6 +36,7 @@ issues:
|
|||
linters:
|
||||
- gocyclo
|
||||
- deadcode
|
||||
- errorlint
|
||||
- path: pkg/integrations/*
|
||||
linters:
|
||||
- gocyclo
|
||||
|
@ -80,3 +82,9 @@ issues:
|
|||
- text: "Missed string"
|
||||
linters:
|
||||
- goheader
|
||||
- path: pkg/.*/error.go
|
||||
linters:
|
||||
- errorlint
|
||||
- path: pkg/models/favorites\.go
|
||||
linters:
|
||||
- nilerr
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"go.testEnvVars": {
|
||||
"VIKUNJA_SERVICE_ROOTPATH": "${workspaceRoot}"
|
||||
}
|
||||
}
|
504
CHANGELOG.md
504
CHANGELOG.md
|
@ -7,6 +7,510 @@ 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.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
|
||||
|
||||
* Don't fail a migration if there is no filter saved ([10ded56](10ded56f6697ef47910ec68d37f26ed47cbe9180))
|
||||
* Don't override saved filters ([beb4d07](beb4d07cf95fc25f7cc5f7471b46bdab49f95fe0))
|
||||
|
||||
## [0.19.1] - 2022-08-17
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Prevent moving a list into a pseudo namespace ([3ccc636](3ccc6365a6892f37ee54b0750a34a61e52f6dba1))
|
||||
* Make sure generating blur hashes for bmp, tiff and webp images works ([8bf0f8b](8bf0f8bb571ddff69a7142be1acaa2e4e0c38e3b))
|
||||
* Add debian-based docker image for arm 32 builds ([c9e044b](c9e044b3ad60d25e9641d22d84571a7db83a26ac))
|
||||
* Only list all users when allowed ([9ddd7f4](9ddd7f48895f508539d591aeebde450a86987024))
|
||||
* Lint ([0c8bed4](0c8bed4054649de8510e5a636d1a14b65d52c402))
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update golang.org/x/sys digest to 6e608f9 (#1229)
|
||||
* *(deps)* Update golang.org/x/sync digest to 886fb93 (#1221)
|
||||
* *(deps)* Update golang.org/x/sys digest to 8e32c04 (#1230)
|
||||
* *(deps)* Update golang.org/x/term digest to a9ba230 (#1222)
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.13.0
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.13.0 (#1231)
|
||||
* *(deps)* Update golang.org/x/sys digest to 1c4a2a7
|
||||
* *(deps)* Update golang.org/x/oauth2 digest to 128564f (#1220)
|
||||
* *(deps)* Update golang.org/x/image digest to 062f8c9 (#1219)
|
||||
* *(deps)* Update golang.org/x/crypto digest to 630584e (#1218)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.8.0 (#1233)
|
||||
* *(deps)* Update golang.org/x/sys digest to fbc7d0a (#1234)
|
||||
* *(deps)* Update module github.com/wneessen/go-mail to v0.2.6 (#1235)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.15 (#1238)
|
||||
|
||||
### Features
|
||||
|
||||
* *(docs)* Add k8s docs* Add openid examples ([dbb0f54](dbb0f5473269fb29c4a484cd233a5b76484c4ca7))
|
||||
* Search by assignee username instead of id ([7f28865](7f28865903740d6dde15ee005323fbdee3072166))
|
||||
* Add migration to change user ids to usernames in saved filters ([3047ccf](3047ccfd4af8fee55d9ebff49138911ab80cb3d2))
|
||||
|
||||
## [0.19.0] - 2022-08-03
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(caldav)* Make sure the caldav tokens of non-local accounts are properly checked
|
||||
* *(caldav)* Properly parse durations when returning VTODOs
|
||||
* *(caldav)* Make sure description is parsed correctly when multiline
|
||||
* *(ci)* Sign drone config
|
||||
* *(ci)* Make sure the linter actually runs
|
||||
* *(ci)* Install git in lint step
|
||||
* *(docker)* Switch to debian base image
|
||||
* *(docker)* Use official go image instead of our own to build
|
||||
* *(docs)* Update minimum required go version
|
||||
* *(docs)* Use up-to-date hugo image for building
|
||||
* *(docs)* Don't use cannonify url
|
||||
* *(docs)* Image urls in synology setup explanation
|
||||
* *(docs)* Clarify frontend requirements to use Vikunja
|
||||
* *(dump)* Don't try to save a config file if none was provided and dump vikunja env variables
|
||||
* *(mage)* Handle different types of errors
|
||||
* *(mail)* Don't set a username by default
|
||||
* *(mail)* Don't try to authenticate against the mail server when no credentials are provided
|
||||
* *(mail)* Set server name in tls config so that sending mail works with skipTlsVerify set to false
|
||||
* *(restore)* Properly decode notifications json data
|
||||
* *(restore)* Make sure to reset sequences after importing a dump when using postgres
|
||||
* *(restore)* Use the correct initial migration* Generate swagger docs ([4de8ec5](4de8ec56a62caef22c2061376383de1fe53ca4c3))
|
||||
* Make sure the full task is available in notifications ([c2b6119](c2b6119434e6e806785d2c259c3ca3d25496ec75))
|
||||
* Don't try to load the namespace of a list if it is a shared list ([d7e47a2](d7e47a28d4bb04d4c7c3ed85a263134180da447a))
|
||||
* Correctly load and pass the user when deleting it ([50b65a5](50b65a517da6869dc6a48fec40323e254ba4c032))
|
||||
* Updating a list might remove its background ([cf05de1](cf05de19b317bd99c30de4c6a149a0d8a4ff4f49))
|
||||
* Sorting for saved filters ([57e5d10](57e5d10eee4c45a04e9e1aaeaf41dd44eb8ce788))
|
||||
* Importing trello attachments ([c3e0e64](c3e0e6405a634894a30dbf9c0506d1691ae4d443))
|
||||
* Lint ([0b77625](0b7762590f6a0a82090ef74e9e7e32b37142d343))
|
||||
* Deleting users with no namespaces ([f8a0a7e](f8a0a7e9539a44b2f790a08eb1b03028b56eaac3))
|
||||
* Importing tasks from todoist without a due time set ([fd0d462](fd0d462bf4dd8225c67ba34958e5148f6167d264))
|
||||
* User deletion never happens ([72d3c54](72d3c54efd3dda6ae846a069415688391cb1c9ae))
|
||||
* User deletion reminder emails counting up ([f581885](f581885e65ada15439ec02f1d18d825b03581523))
|
||||
* User not actually deleted ([70e005e](70e005e7ce5cf1dd25ec9ddfde3cfbbd258fadb6))
|
||||
* User deletion schedule ([5c88dfe](5c88dfe88eab442724f22c3b29741e78939deae2))
|
||||
* Friendly name not getting synced on first login from openid ([190a9f2](190a9f2a4c1a59bc68b839c465bb2536532c0e96))
|
||||
* Importing archived lists or namespaces ([8bb3f8d](8bb3f8d37c78dc704ff4316c750e143528151b48))
|
||||
* Lint ([a31086a](a31086a7a9ca7723f61a826bccbea125243478f1))
|
||||
* Microsoft todo migration not importing all tasks ([43f1daf](43f1daf40c388a0aa40f7fd6a8db4c78308d4efd))
|
||||
* Clarify which config file is used on startup ([44aaf0a](44aaf0a4eccebb1d1a25f5563e928bd1bb82d351))
|
||||
* Disabling logging completely now works ([22e3f24](22e3f242a396aa9cf54e9426077816f97a0da36f))
|
||||
* Restoring dumps with no config file saved in them ([8bf2254](8bf2254f4b87446ab0a39080cb0b7d32ccec7c0a))
|
||||
* Validate email address when creating a user via cli ([75f74b4](75f74b429eea7ae3a75cb10def1ca658af35086a))
|
||||
* Checking for error types ([ac6818a](ac6818a4769a162c458553944509fe64357370f9))
|
||||
* Lint ([7fa0865](7fa086518800243385d8cc4696eeea9bf093e5b3))
|
||||
* Return BlurHash in unsplash search results ([6b51fae](6b51fae0931308464038f55b25e81e68d014c49c))
|
||||
* Go mod tidy ([e19ad11](e19ad1184662dc9ac9aa89a44abdffc091e2a1b8))
|
||||
* Decoding images for blurHash generation ([d3bdafb](d3bdafb717b1ad3e2165097ef0b0c2dd47e1502e))
|
||||
* Lint ([de97fcb](de97fcbd121b1d56b74175fd79ef594ef34e71c8))
|
||||
* Broken link (#27) ([96e519e](96e519ea96c9537222d0b455037e11fbe9660c31))
|
||||
* Add more methods to figure out the current binary location ([9845fcc](9845fcc1708431f8f736d36e7e19a1067b0e0e52))
|
||||
* Set derived default values only after reading config from file or env ([f5ebada](f5ebada91351faf1e5602f0260908defaaabd810))
|
||||
* Sort tasks logically and consistent across dbms (#1177) ([e52c45d](e52c45d5aabb74ea7b472e8d5b44491cdd7e9489))
|
||||
* VIKUNJA_SERVICE_JWT_SECRET should be VIKUNJA_SERVICE_JWTSECRET (#1184) ([172a621](172a6214d7c30278017129b950339c78a6ddb7bc))
|
||||
* Add missing migration ([d837f8a](d837f8a6248b5ff2700a4bfc300d7f9d466cb918))
|
||||
* Revert renaming Attachments to Embeds everywhere ([c62e26b](c62e26b6fe9d9f362fcfb1df2d5664d7f6854c31))
|
||||
* Set the correct go version in go.mod ([bc7f6a8](bc7f6a858693b0e61fff7d03b5c2b40b6ae1a55d))
|
||||
* Go mod tidy ([7a30294](7a30294407843693f6c3a7414b3b9d7093359194))
|
||||
* Tests ([d0e09d6](d0e09d69d048e62ee7c5b666c2f56761b03e68e6))
|
||||
* Go mod tidy ([951d74b](951d74b272b1e881faa10095f47b6598bb076273))
|
||||
* Prevent logging openid provider errors twice ([25ffa1b](25ffa1bc2e2f1108f20b0336708d2410bb61c9e1))
|
||||
* Remove credential escaping for postgres connections to allow for passwords with special characters ([230478a](230478aae947c86f4c6f1f251dcb30aeb1293283))
|
||||
* Cycles in tasks array when memory caching was enabled ([f5a4c13](f5a4c136fbca6fc5770476e6de8d81173f007df2))
|
||||
* Add missing error check ([5cc4927](5cc4927b9ef97667bf763772beb36225fdbeded8))
|
||||
* Properly set tls config for mailer ([5743a4a](5743a4afe51de221beeeabe66552ae4d92eed1a6))
|
||||
* Return 9:00 as default time for reminders if none was set ([79b3167](79b31673e2a79eaa124976840e85757d2bebb887))
|
||||
* Reset id sequence when importing a dump from postgres ([0f555b7](0f555b7ec74ad493d2f70a4f4040db333943dc1c))
|
||||
* Add validation for negative repeat after values ([dd46174](dd461746a655d716ef142d96a2bcef5615de3dd9))
|
||||
* Lint ([1feb62c](1feb62cc458e939d46d16d24347557e7959ddfb9))
|
||||
* Make sure to use user discoverability settings when searching list users ([382a788](382a7884be1f37da5c8f657c4b17316d8691dd59))
|
||||
* Properly decode params in url ([8f27e7e](8f27e7e619ac73716211d838f52c73d7d97aead5))
|
||||
* Return all users on a list when no search param was provided ([c51ee94](c51ee94ad1d552d69c71adfc2180c7ad0d23235d))
|
||||
* Don't return email addresses from user search results ([3688bbd](3688bbde20e989397353ea4f7e872b00a53099c2))
|
||||
* Lint ([77fafd5](77fafd5dc32aee464961be40d5d0ccf82490d02a))
|
||||
* Increase test timeout ([26e2d0b](26e2d0bddeaea902dba055baf7a4c866a44ba7f1))
|
||||
* Switch to buster for build image ([59796fd](59796fd4905fca74d26c5541878379cda143a30e))
|
||||
* Use our own build image as base build image ([b6d7323](b6d7323cdfac958c9740feba1342114ab13a0afd))
|
||||
* Use golang build image to test migrations ([84bcdbf](84bcdbf937c3be7823fcf8d5fef52e3cbb1c9bde))
|
||||
* Switch back to alpine for everything, disable arm 32 docker builds ([7ffe9b6](7ffe9b625e441202a704db2774dd66fc38244c6d))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update golang.org/x/sys commit hash to a851e7d (#972)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to aa78b53 (#973)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 528a39c (#974)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 437939a (#975)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.1 (#976)
|
||||
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.1.0 (#985)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.9.0 (#987)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 089bfa5 (#979)
|
||||
* *(deps)* Update golang.org/x/term commit hash to 140adaa (#983)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.0 (#988)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to b8560ed (#989)
|
||||
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.1.0 (#991)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 92d5a99 (#992)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.7.3 (#990)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.1 (#993)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 1cf2251 (#994)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 39ccf1d (#995)
|
||||
* *(deps)* Update golang.org/x/term commit hash to 03fcf44 (#996)
|
||||
* *(deps)* Update golang.org/x/oauth2 commit hash to 6b3c2da (#1000)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 69063c4 (#1001)
|
||||
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.0 (#1004)
|
||||
* *(deps)* Update postgres docker tag to v14 (#1005)
|
||||
* *(deps)* Update module github.com/go-redis/redis/v8 to v8.11.4 (#1003)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.9 (#1008)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 9d821ac (#1009)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 0ec99a6 (#1010)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 9d61738 (#1011)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.2 (#1012)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 8e51046 (#1016)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to d6a326f (#1017)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.7.4 (#1018)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 711f33c (#1019)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 69cdffd (#1020)
|
||||
* *(deps)* Update golang.org/x/oauth2 commit hash to ba495a6 (#1022)
|
||||
* *(deps)* Update golang.org/x/image commit hash to 6944b10 (#1023)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 6e78728 (#1024)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to b3129d9 (#1025)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 611d5d6 (#1026)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 39c9dd3 (#1027)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to a2f17f7 (#1028)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 4dd7244 (#1029)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to ae416a5 (#1030)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 7861aae (#1031)
|
||||
* *(deps)* Update golang.org/x/oauth2 commit hash to d3ed0bb (#1032)
|
||||
* *(deps)* Update module github.com/labstack/gommon to v0.3.1 (#1033)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to c75c477 (#1034)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to ebca88c (#1035)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to e0b2ad0 (#1037)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.3 (#1038)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to ceb1ce7 (#1041)
|
||||
* *(deps)* Update module github.com/lib/pq to v1.10.4 (#1040)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 51b60fd (#1042)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 99a5385 (#1043)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to f221eed (#1044)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 0c823b9 (#1045)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.4 (#1046)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 0a5406a (#1048)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to b4de73f (#1047)
|
||||
* *(deps)* Update module github.com/ulule/limiter/v3 to v3.9.0 (#1049)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to ae814b3 (#1050)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to dee7805 (#1051)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to ef496fb (#1052)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to fe61309 (#1054)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.7.6 (#1055)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 5770296 (#1056)
|
||||
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.2.0 (#1057)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 94396e4 (#1058)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 97ca703 (#1059)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 4570a08 (#1062)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 798191b (#1061)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to af8b642 (#1063)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.10.0 (#1064)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 03aa0b5 (#1067)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 3b038e5 (#1068)
|
||||
* *(deps)* Update module github.com/spf13/cobra to v1.3.0 (#1070)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 4825e8c (#1071)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.10.1 (#1072)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to e495a2d (#1073)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 4abf325 (#1074)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 1d35b9e (#1075)
|
||||
* *(deps)* Update module github.com/magefile/mage to v1.12.0 (#1076)
|
||||
* *(deps)* Update module github.com/magefile/mage to v1.12.1 (#1077)
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.12.0 (#1079)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.7.8 (#1080)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.7.0 (#1078)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.7.1 (#1081)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.10 (#1082)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.8.0 (#1083)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.2 (#1084)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.6.3 (#1089)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to a018aaa (#1088)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 5a964db (#1090)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 5e0467b (#1091)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to da31bd3 (#1093)
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.12.0 (#1094)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to e04a857 (#1097)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to aa10faf (#1098)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.11 (#1099)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 198e437 (#1100)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 99c3d69 (#1101)
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.12.1 (#1102)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.92.0 (#1103)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.92.1 (#1104)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 30dcbda (#1105)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.7.9 (#1106)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 1c1b9b1 (#1107)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.8.1 (#1108)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 5739886 (#1110)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 20e1d8d (#1111)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.5 (#1112)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to bba287d (#1113)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to dad3315 (#1114)
|
||||
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.3.0 (#1117)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 3681064 (#1116)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to db63837 (#1115)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to f4118a5 (#1118)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to 8634188 (#1121)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.6 (#1122)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.7 (#1123)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.0 (#1124)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 0005352 (#1125)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to f242548 (#1126)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.92.2 (#1127)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to dbe011f (#1129)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 95c6836 (#1130)
|
||||
* *(deps)* Update golang.org/x/oauth2 commit hash to ee48083 (#1128)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.12 (#1132)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 4e6760a (#1131)
|
||||
* *(deps)* Update golang.org/x/image commit hash to 723b81c (#1133)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.7.0 (#1134)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.93.0 (#1135)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.8 (#1136)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.93.2 (#1137)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to 22a9840 (#1138)
|
||||
* *(deps)* Update golang.org/x/crypto commit hash to efcb850 (#1139)
|
||||
* *(deps)* Update golang.org/x/oauth2 commit hash to 6242fa9 (#1140)
|
||||
* *(deps)* Update golang.org/x/sys commit hash to b874c99 (#1141)
|
||||
* *(deps)* Update klakegg/hugo docker tag to v0.93.3 (#1142)
|
||||
* *(deps)* Update module github.com/labstack/echo/v4 to v4.7.1 (#1146)
|
||||
* *(deps)* Update module github.com/stretchr/testify to v1.7.1 (#1148)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.1 (#1156)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.11 (#1143)
|
||||
* *(deps)* Update module github.com/spf13/cobra to v1.4.0 (#1145)
|
||||
* *(deps)* Update module github.com/lib/pq to v1.10.5 (#1157)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.11.0 (#1159)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.12 (#1162)
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.12.2 (#1166)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.13 (#1165)
|
||||
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.2.0 (#1164)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.2 (#1167)
|
||||
* *(deps)* Update module github.com/lib/pq to v1.10.6 (#1169)
|
||||
* *(deps)* Update module gopkg.in/yaml.v3 to v3.0.1 (#1179)
|
||||
* *(deps)* Update module github.com/imdario/mergo to v0.3.13 (#1178)
|
||||
* *(deps)* Update module github.com/stretchr/testify to v1.7.2 (#1182)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.3 (#1185)
|
||||
* *(deps)* Update module github.com/spf13/cobra to v1.5.0 (#1192)
|
||||
* *(deps)* Update module github.com/golang-jwt/jwt/v4 to v4.4.2 (#1193)
|
||||
* *(deps)* Update module github.com/stretchr/testify to v1.8.0 (#1191)
|
||||
* *(deps)* Update module github.com/go-testfixtures/testfixtures/v3 to v3.8.0 (#1168)
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.14 (#1194)
|
||||
* *(deps)* Update golang.org/x/term digest to 065cf7b (#1207)
|
||||
* *(deps)* Update golang.org/x/image digest to 41969df (#1203)
|
||||
* *(deps)* Update module github.com/yuin/goldmark to v1.4.13 (#1209)
|
||||
* *(deps)* Update golang.org/x/crypto digest to 0559593 (#1202)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.9.0 (#1210)
|
||||
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.1 (#1208)
|
||||
* *(deps)* Update golang.org/x/sync digest to 0de741c (#1205)
|
||||
* *(deps)* Update github.com/c2h5oh/datasize digest to 859f65c (#1201)
|
||||
* *(deps)* Update golang.org/x/oauth2 digest to 2104d58 (#1204)
|
||||
* *(deps)* Update golang.org/x/sys digest to c0bba94 (#1206)
|
||||
* *(deps)* Update golang.org/x/oauth2 digest to c8730f7 (#1214)
|
||||
* *(deps)* Update module github.com/spf13/afero to v1.9.2 (#1215)
|
||||
* *(deps)* Update module github.com/swaggo/swag to v1.8.4 (#1216)
|
||||
* *(deps)* Update module github.com/spf13/viper to v1.12.0 (#1180)
|
||||
* *(deps)* Update golang.org/x/sys digest to 1609e55 (#1217)
|
||||
* *(deps)* Update module github.com/go-testfixtures/testfixtures/v3 to v3.8.1 (#1226)
|
||||
* *(deps)* Update module go to 1.18 (#1225)
|
||||
|
||||
### Documentation
|
||||
* Add docker-compose example with no proxy ([4255bc3](4255bc3a945b6fe4314e3cd3f62908dd1be1ff4a))
|
||||
* Add another youtube tutorial ([dbd6f36](dbd6f36da6e56355993cc1411379997e26c88b36))
|
||||
* Fix api url in docker examples without a proxy ([68998e9](68998e90a446569869fb150bd5fc0739f496b066))
|
||||
* Make sure all links to vikunja pages are https ([cc612d5](cc612d505f22e5d895b6ebda61fe62498634cec5))
|
||||
* Update backup instructions ([4829c89](4829c899400544ad27cacfb7d19b40988302a413))
|
||||
* Add postgres to docker-compose examples ([2aea169](2aea1691cf33b7d9e03fbe2c711af7d8f76d9724))
|
||||
* Improve development docs ([9bf32aa](9bf32aae99a7e69cce0cd4477e8fc8ddcaea25ea))
|
||||
* Add another tutorial link ([1fa74cb](1fa74cba6407c2b694b14f8439f1492476433d62))
|
||||
* Improve wording for systemd ([13561f2](13561f211493903b17c856b3010345ea9df725d4))
|
||||
* Update testing ([da318e3](da318e3db15121ba864db8450a76ba9ed18b9fd5))
|
||||
* Add guide for Synology NAS ([049ae39](049ae39c62079f77921b7a9fad5023b2c1c0c1c5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* *(docs)* Add details of using NGINX Proxy Manager to the Reverse Proxy docs (#13)
|
||||
* *(docs)* Add versions explanation
|
||||
* *(mail)* Don't try to authenticate when no username and password was provided* Add better error logs for mage commands ([bb086eb](bb086eb9f87669f844c283d42ea9ca9f3f5a7877))
|
||||
* Expose if task comments are enabled or not in /info ([ae8db17](ae8db176db57fa6176e00b87924f70352332ca66))
|
||||
* Improve account deletion email grammar (#1006) ([dcb52c0](dcb52c00f1c6b3217e2b508d7799fc83adb3b055))
|
||||
* Add more debug logging when deleting users ([8f55af0](8f55af07c936218487ec94e65c6673fbddd0cdb5))
|
||||
* Don't require a password for data export from users authenticated with third-party auth ([9eca971](9eca971c938699d481915fb6e14c765aea1fa3b5))
|
||||
* Expose if a user is a local user through its jwt token ([516c812](516c812043e77be7f834ae1326d13d39e156ef77))
|
||||
* Expose if a user is a local user through the /user endpoint ([2683ef2](2683ef23d538eb846d5d799798fa82cca70dc017))
|
||||
* Enable rate limit for unauthenticated routes ([093d0c6](093d0c65ca6338358dbd1df904daadd7808f2817))
|
||||
* Use wallpaper topic for default unsplash background list ([88a2ced](88a2cede19f1844814530af948c3cc5a0b026419))
|
||||
* Gravatar - Lowercase emails before MD5 hash (#10) ([36bf3d2](36bf3d216a7be28e917e2816a9e5da43439f2c20))
|
||||
* Add marble avatar (#1060) ([73ee696](73ee696fc3cf941af2d2c2cf81224aa01f93234e))
|
||||
* Save user language in the settings ([a98119f](a98119f2d670a11efab6008129b767f9208f8113))
|
||||
* Add time zone setting for reminders (#1092) ([61d49c3](61d49c3a56a59e52ce407b858ddd4aa573dbee9d))
|
||||
* Add long-lived api tokens (#1085) ([1322cb1](1322cb16d76a40ad90631e3e091da0f0d44957a9))
|
||||
* Upgrade golangci-lint to 1.45.2 ([5cf263a](5cf263a86f954a38cbfafb6b0857bf591f82a811))
|
||||
* Add date math for filters (#1086) ([0a1d8c9](0a1d8c940410b03a78016ac6110883ca05484816))
|
||||
* Add migration to create BlurHash strings for all list backgrounds ([362706b](362706b38d52720b5a1615e185a985b7708168f7))
|
||||
* Generate a BlurHash when uploading a new image ([f83b09a](f83b09af59ed25425a16824ccf48d903c81e861a))
|
||||
* Save BlurHash from unsplash when selecting a photo from unsplash ([2ec7d7a](2ec7d7a8a85cc12c07d20cfab9b90a78a7857eb6))
|
||||
* Return BlurHash for unsplash search results ([6df8658](6df865876df961f2bec476126bf6e7fbe5d43e0e))
|
||||
* Add caldav tokens (#1065) ([e4b50e8](e4b50e84a44f809cc829c2fdb6f52b03b40a367b))
|
||||
* Ability to serve static files (#1174) ([acaa850](acaa85083f2bebbc67608ae0f454ed5e9a3ef8a0))
|
||||
* Restrict max avatar size ([2f25b48](2f25b48869f59256bf7d692c4486c64c30b85e5e))
|
||||
* Send overdue tasks email notification at 9:00 in the user's time zone ([7eb3b96](7eb3b96a4465ca6648572b07c506c06f2c28c375))
|
||||
* Add setting to change overdue tasks reminder email time ([8869adf](8869adfc276f674b686bf68f949d7efbb417e55b))
|
||||
* Allow only the authors of task comments to edit them ([01271c4](01271c4c0111b3b040dcb9a0d502d31078ad6d4b))
|
||||
* Migrate away from gomail ([30e0e98](30e0e98f7738e36698990523377f47edcbf6806c))
|
||||
* Embed the vikunja logo as inline attachment ([f4f8450](f4f8450d166f1a836eea202dd0340d2156d3dfe9))
|
||||
* Use embed fs directly to embed the logo in mails ([73c4c39](73c4c399e5d610bb713f1e9feab543e0425ee959))
|
||||
* Use actual uuids for tasks ([62325de](62325de9cd5da5b70987081956a28e7baa907081))
|
||||
* Add issue template ([117f6b3](117f6b38e1d35c09f2657975ea75dcfedcd8425d))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(ci)* Use latest version of s3 plugin
|
||||
* *(ci)* Sign drone config
|
||||
* *(docs)* Update docs about compiling from source
|
||||
* *(docs)* Redirect properly from /docs/docs
|
||||
* *(docs)* Add new mailer option to docs
|
||||
* *(docs)* Clarify openid setup with environment variables
|
||||
* *(docs)* Add frontendurl to all example configs
|
||||
* *(mage)* Don't set api packages when they are not used* Sign drone config ([1d8d0f1](1d8d0f140e4f2a59947167bd597e5f12b84b009d))
|
||||
* Cleanup namespace creation ([b60c69c](b60c69c5a8c004a780b989cf0bb8ab6455086b0f))
|
||||
* Generate swagger docs ([ba2bdff](ba2bdff39109db9ecc4b525e39e2642b41ac03b8))
|
||||
* Go mod tidy ([726a517](726a517bec731f1af8e3186e280718fef02cadf7))
|
||||
* Upgrade trello api wrapper and remove fork ([7e99618](7e99618319547c7e7dfa2cc063f654300f7074fb))
|
||||
* Use our custom build image to build docker image ([251b877](251b877015761fdd2b8dbd18cd8ec696dc374103))
|
||||
* Update golangci-lint ([430057a](430057a404b04e75c62a15693f479c6fc8e63189))
|
||||
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* Healthcheck endpoint (#998)
|
||||
* *(other)* Added the ability to configure the JWT expiry date using a new server.jwtttl config parameter. (#999)
|
||||
* *(other)* Enable a list to be moved across namespaces (#1096)
|
||||
* *(other)* A bunch of dependency updates at once (#1155)
|
||||
* *(other)* Add client-cert parameters of the Go pq driver to the Vikunja config (#1161)
|
||||
* *(other)* Add exec to run script to run app as PID 1 (#1200)
|
||||
|
||||
## [0.18.1] - 2021-09-08
|
||||
|
||||
### Fixed
|
||||
|
||||
* Docs: Add another third-party tutorial link
|
||||
* Don't try to export items which do not have a parent
|
||||
* fix(deps): update golang.org/x/sys commit hash to 6f6e228 (#970)
|
||||
* fix(deps): update golang.org/x/sys commit hash to c212e73 (#971)
|
||||
* Fix exporting tasks from archived lists
|
||||
* Fix lint
|
||||
* Fix tasks not exported
|
||||
* Fix tmp export file created in the wrong path
|
||||
|
||||
## [0.18.0] - 2021-09-05
|
||||
|
||||
### Added
|
||||
|
|
20
Dockerfile
20
Dockerfile
|
@ -1,29 +1,27 @@
|
|||
|
||||
##############
|
||||
# Build stage
|
||||
FROM golang:1-alpine3.12 AS build-env
|
||||
FROM golang:1.19-alpine AS build-env
|
||||
|
||||
RUN apk --no-cache add build-base git && \
|
||||
go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
|
||||
ARG VIKUNJA_VERSION
|
||||
ENV TAGS "sqlite"
|
||||
ENV GO111MODULE=on
|
||||
|
||||
# Build deps
|
||||
RUN apk --no-cache add build-base git
|
||||
|
||||
# Setup repo
|
||||
COPY . ${GOPATH}/src/code.vikunja.io/api
|
||||
WORKDIR ${GOPATH}/src/code.vikunja.io/api
|
||||
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 \
|
||||
&& go install github.com/magefile/mage \
|
||||
&& 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.
|
||||
FROM alpine:3.12
|
||||
FROM alpine:3.16
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
|
||||
WORKDIR /app/vikunja/
|
||||
|
@ -39,7 +37,7 @@ RUN apk --no-cache add shadow && \
|
|||
chown vikunja -R /app/vikunja
|
||||
COPY run.sh /run.sh
|
||||
|
||||
# Fix time zone settings not working
|
||||
# Add time zone data
|
||||
RUN apk --no-cache add tzdata
|
||||
|
||||
# Files permissions
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
##############
|
||||
# Build stage
|
||||
FROM golang:1.19-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 @@
|
|||
|
||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone.kolaente.de/vikunja/api)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.18.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.20.0-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/api.svg)](https://hub.docker.com/r/vikunja/api/)
|
||||
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
|
||||
[![Go Report Card](https://goreportcard.com/badge/kolaente.dev/vikunja/api)](https://goreportcard.com/report/kolaente.dev/vikunja/api)
|
||||
|
@ -56,6 +56,10 @@ See [the roadmap](https://my.vikunja.cloud/share/QFyzYEmEYfSyQfTOmIRSwLUpkFjboaB
|
|||
|
||||
Fork -> Push -> Pull-Request. Also see the [dev docs](https://vikunja.io/docs/development/) for more info.
|
||||
|
||||
## Sponsors
|
||||
|
||||
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the AGPLv3 License. See the [LICENSE](LICENSE) file for the full license text.
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
[changelog]
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
* *({{commit.scope}})* {{ commit.message | upper_first }}
|
||||
{%- if commit.breaking %}
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
* {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
|
||||
{% if commit.breaking -%}
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{% endfor %}\n
|
||||
|
||||
"""
|
||||
#{% for group, commits in commits | group_by(attribute="group") %}
|
||||
# ### {{ group | upper_first }}
|
||||
# {% for commit in commits %}\
|
||||
# - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
|
||||
# {% endfor %}\
|
||||
#{% endfor %}\n
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = false
|
||||
commit_parsers = [
|
||||
{ message = ".*(deps).*", group = "Dependencies"},
|
||||
{ message = "^feat", group = "Features"},
|
||||
{ message = "^fix", group = "Bug Fixes"},
|
||||
{ message = "^doc", group = "Documentation"},
|
||||
{ message = "^perf", group = "Performance"},
|
||||
{ message = "^refactor", group = "Refactor"},
|
||||
{ message = "^style", group = "Styling"},
|
||||
{ message = "^test", group = "Testing"},
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||
{ body = ".*security", group = "Security"},
|
||||
{ message = ".*", group = "Other", default_scope = "other"}, # Everything that's not a conventional commit goes into the "Other" category
|
||||
]
|
||||
|
|
@ -3,6 +3,13 @@ service:
|
|||
# Default is a random token which will be generated at each startup of vikunja.
|
||||
# (This means all already issued tokens will be invalid once you restart vikunja)
|
||||
JWTSecret: "<jwt-secret>"
|
||||
# The duration of the issed JWT tokens in seconds.
|
||||
# The default is 259200 seconds (3 Days).
|
||||
jwtttl: 259200
|
||||
# The duration of the "remember me" time in seconds. When the login request is made with
|
||||
# the long param set, the token returned will be valid for this period.
|
||||
# The default is 2592000 seconds (30 Days).
|
||||
jwtttllong: 2592000
|
||||
# The interface on which to run the webserver
|
||||
interface: ":3456"
|
||||
# Path to Unix socket. If set, it will be created and used instead of tcp
|
||||
|
@ -15,6 +22,8 @@ service:
|
|||
# Vikunja will also look in this path for a config file, so you could provide only this variable to point to a folder
|
||||
# with a config file which will then be used.
|
||||
rootpath: <rootpath>
|
||||
# Path on the file system to serve static files from. Set to the path of the frontend files to host frontend alongside the api.
|
||||
staticpath: ""
|
||||
# The max number of items which can be returned per page
|
||||
maxitemsperpage: 50
|
||||
# Enable the caldav endpoint, see the docs for more details
|
||||
|
@ -47,17 +56,20 @@ service:
|
|||
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||
# for user deletion.
|
||||
enableuserdeletion: true
|
||||
# The maximum size clients will be able to request for user avatars.
|
||||
# If clients request a size bigger than this, it will be changed on the fly.
|
||||
maxavatarsize: 1024
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
type: "sqlite"
|
||||
# Database user which is used to connect to the database.
|
||||
user: "vikunja"
|
||||
# Databse password
|
||||
# Database password
|
||||
password: ""
|
||||
# Databse host
|
||||
# Database host
|
||||
host: "localhost"
|
||||
# Databse to use
|
||||
# Database to use
|
||||
database: "vikunja"
|
||||
# When using sqlite, this is the path where to store the data
|
||||
path: "./vikunja.db"
|
||||
|
@ -70,6 +82,12 @@ database:
|
|||
# Secure connection mode. Only used with postgres.
|
||||
# (see https://pkg.go.dev/github.com/lib/pq?tab=doc#hdr-Connection_String_Parameters)
|
||||
sslmode: disable
|
||||
# The path to the client cert. Only used with postgres.
|
||||
sslcert: ""
|
||||
# The path to the client key. Only used with postgres.
|
||||
sslkey: ""
|
||||
# The path to the ca cert. Only used with postgres.
|
||||
sslrootcert: ""
|
||||
# Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
|
||||
tls: false
|
||||
|
||||
|
@ -109,8 +127,11 @@ 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"
|
||||
# SMTP username
|
||||
username: "user"
|
||||
# SMTP password
|
||||
|
@ -281,6 +302,8 @@ auth:
|
|||
enabled: false
|
||||
# The url to redirect clients to. Defaults to the configured frontend url. If you're using Vikunja with the official
|
||||
# frontend, you don't need to change this value.
|
||||
# **Note:** The redirect url must exactly match the configured redirect url with the third party provider.
|
||||
# This includes all slashes at the end or protocols.
|
||||
redirecturl: <frontend url>
|
||||
# A list of enabled providers
|
||||
providers:
|
||||
|
@ -301,3 +324,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>
|
||||
|
|
|
@ -2,7 +2,7 @@ baseurl: https://vikunja.io/docs/
|
|||
title: Vikunja
|
||||
theme: vikunja
|
||||
enableRobotsTXT: true
|
||||
canonifyURLs: true
|
||||
canonifyURLs: false
|
||||
|
||||
pygmentsUseClasses: true
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ menu:
|
|||
|
||||
# Database
|
||||
|
||||
Vikunja uses [xorm](http://xorm.io/) as an abstraction layer to handle the database connection.
|
||||
Please refer to [their](http://xorm.io/docs/) documentation on how to exactly use it.
|
||||
Vikunja uses [xorm](https://xorm.io/) as an abstraction layer to handle the database connection.
|
||||
Please refer to [their](https://xorm.io/docs/) documentation on how to exactly use it.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
|
@ -24,7 +24,7 @@ In other packages, use the `db.NewSession()` method to get a new database sessio
|
|||
|
||||
To add a new table to the database, create the struct and [add a migration for it]({{< ref "db-migrations.md" >}}).
|
||||
|
||||
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentaion](http://xorm.io/docs/).
|
||||
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentaion](https://xorm.io/docs/).
|
||||
|
||||
In most cases you will also need to implement the `TableName() string` method on the new struct to make sure the table
|
||||
name matches the rest of the tables - plural.
|
||||
|
|
|
@ -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
|
||||
|
@ -12,11 +12,44 @@ menu:
|
|||
|
||||
# Development
|
||||
|
||||
We use go modules to manage third-party libraries for Vikunja, so you'll need at least go `1.11` to use these.
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## General
|
||||
|
||||
To contribute to Vikunja, fork the project and work on the main branch.
|
||||
Once you feel like your changes are ready, open a PR in the respective repo.
|
||||
A maintainer will take a look and give you feedback. Once everyone is happy, the PR gets merged and released.
|
||||
|
||||
If you plan to do a bigger change, it is better to open an issue for discussion first.
|
||||
|
||||
## API
|
||||
|
||||
The code for the api is located at [code.vikunja.io/api](https://code.vikunja.io/api).
|
||||
|
||||
We use go modules to manage third-party libraries for Vikunja, so you'll need at least go `1.17` to use these.
|
||||
|
||||
A lot of developing tasks are automated using a Magefile, so make sure to [take a look at it]({{< ref "mage.md">}}).
|
||||
|
||||
Make sure to check the other doc articles for specific development tasks like [testing]({{< ref "test.md">}}),
|
||||
Make sure to check the other doc articles for specific development tasks like [testing]({{< ref "test.md">}}),
|
||||
[database migrations]({{< ref "db-migrations.md" >}}) and the [project structure]({{< ref "structure.md" >}}).
|
||||
|
||||
## 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 [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.
|
||||
|
||||
A release gets tagged from the main branch with the version name as tag name.
|
||||
|
||||
Backports and point-releases should go to a `release/version` branch, based on the tag they are building on top of.
|
||||
|
||||
## Conventional commits
|
||||
|
||||
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.
|
||||
|
|
|
@ -11,9 +11,9 @@ menu:
|
|||
# Mage
|
||||
|
||||
Vikunja uses [Mage](https://magefile.org/) to script common development tasks and even releasing.
|
||||
Mage is a pure go solution which allows for greater flexibility and things like better paralelization.
|
||||
Mage is a pure go solution which allows for greater flexibility and things like better parallelization.
|
||||
|
||||
This document explains what taks are available and what they do.
|
||||
This document explains what tasks are available and what they do.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
|
|
|
@ -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 nessecary 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
|
||||
|
|
@ -10,32 +10,31 @@ menu:
|
|||
|
||||
# Testing
|
||||
|
||||
You can run unit tests with [mage]({{< ref "mage.md">}}) with
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## API Tests
|
||||
|
||||
The following parts are about the kinds of tests in the API package and how to run them.
|
||||
|
||||
### Prerequesites
|
||||
|
||||
To run any kind of test, you need to specify Vikunja's [root path](https://vikunja.io/docs/config-options/#rootpath).
|
||||
This is required to make sure all test fixtures are correctly loaded.
|
||||
|
||||
The easies way to do that is to set the environment variable `VIKUNJA_SERVICE_ROOTPATH` to the path where you cloned the working directory.
|
||||
|
||||
### Unit tests
|
||||
|
||||
To run unit tests with [mage]({{< ref "mage.md">}}), execute
|
||||
|
||||
{{< highlight bash >}}
|
||||
mage test:unit
|
||||
{{< /highlight >}}
|
||||
|
||||
{{< table_of_contents >}}
|
||||
In Vikunja, everything that is not an integration test counts as unit test - even if it accesses the db.
|
||||
This definition is a bit blurry, but we haven't found a better one yet.
|
||||
|
||||
## Running tests with config
|
||||
|
||||
You can run tests with all available config variables if you want, enabeling you to run tests for a lot of scenarios.
|
||||
|
||||
To use the normal config set the enviroment variable `VIKUNJA_TESTS_USE_CONFIG=1`.
|
||||
|
||||
## Show sql queries
|
||||
|
||||
When `UNIT_TESTS_VERBOSE=1` is set, all sql queries will be shown when tests are run.
|
||||
|
||||
## Fixtures
|
||||
|
||||
All tests are run against a set of db fixtures.
|
||||
These fixtures are defined in `pkg/models/fixtures` in YAML-Files which represent the database structure.
|
||||
|
||||
When you add a new test case which requires new database entries to test against, update these files.
|
||||
|
||||
## Integration tests
|
||||
### Integration tests
|
||||
|
||||
All integration tests live in `pkg/integrations`.
|
||||
You can run them by executing `mage test:integration`.
|
||||
|
@ -45,7 +44,25 @@ see at the beginning of this document.
|
|||
|
||||
To run integration tests, use `mage test:integration`.
|
||||
|
||||
## Initializing db fixtures when writing tests
|
||||
### Running tests with config
|
||||
|
||||
You can run tests with all available config variables if you want, enabeling you to run tests for a lot of scenarios.
|
||||
We use this in CI to run all tests with different databases.
|
||||
|
||||
To use the normal config set the enviroment variable `VIKUNJA_TESTS_USE_CONFIG=1`.
|
||||
|
||||
### Showing sql queries
|
||||
|
||||
When the environment variable `UNIT_TESTS_VERBOSE=1` is set, all sql queries will be shown during the test run.
|
||||
|
||||
### Fixtures
|
||||
|
||||
All tests are run against a set of db fixtures.
|
||||
These fixtures are defined in `pkg/models/fixtures` in YAML-Files which represent the database structure.
|
||||
|
||||
When you add a new test case which requires new database entries to test against, update these files.
|
||||
|
||||
#### Initializing db fixtures when writing tests
|
||||
|
||||
All db fixtures for all tests live in the `pkg/db/fixtures/` folder as yaml files.
|
||||
Each file has the same name as the table the fixtures are for.
|
||||
|
@ -54,19 +71,39 @@ You should put new fixtures in this folder.
|
|||
When initializing db fixtures, you are responsible for defining which tables your package needs in your test init function.
|
||||
Usually, this is done as follows (this code snippet is taken from the `user` package):
|
||||
|
||||
```go
|
||||
{{< highlight go >}}
|
||||
err = db.InitTestFixtures("users")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
In your actual tests, you then load the fixtures into the in-memory db like so:
|
||||
|
||||
```go
|
||||
{{< highlight go >}}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
This will load all fixtures you defined in your test init method.
|
||||
You should always use this method to load fixtures, the only exception is when your package tests require extra test
|
||||
fixtures other than db fixtures (like files).
|
||||
|
||||
## Frontend tests
|
||||
|
||||
The frontend has end to end tests with Cypress that use a Vikunja instance and drive a browser against it.
|
||||
Check out the docs [in the frontend repo](https://kolaente.dev/vikunja/frontend/src/branch/main/cypress/README.md) about how they work and how to get them running.
|
||||
|
||||
### Unit Tests
|
||||
|
||||
To run the frontend unit tests, run
|
||||
|
||||
{{< highlight bash >}}
|
||||
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 >}}
|
||||
pnpm run test:unit-watch
|
||||
{{< /highlight >}}
|
||||
|
|
|
@ -73,4 +73,4 @@ Beispiel: „Benutzer:in“
|
|||
|
||||
## Weiterführende Links
|
||||
|
||||
* http://docs.translatehouse.org/projects/localization-guide/en/latest/guide/translation_guidelines_german.html
|
||||
* https://docs.translatehouse.org/projects/localization-guide/en/latest/guide/translation_guidelines_german.html
|
||||
|
|
|
@ -10,12 +10,17 @@ menu:
|
|||
|
||||
# What to backup
|
||||
|
||||
Vikunja does not store any data outside of the database.
|
||||
So, all you need to backup are the contents of that database and maybe the config file.
|
||||
There are two parts you need to back up: The database and attachment files.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## MySQL
|
||||
## Files
|
||||
|
||||
To back up attachments and other files, it is enough to copy them [from the attachments folder]({{< ref "config.md" >}}#basepath) to some other place.
|
||||
|
||||
## Database
|
||||
|
||||
### MySQL
|
||||
|
||||
To create a backup from mysql use the `mysqldump` command:
|
||||
|
||||
|
@ -31,7 +36,7 @@ To restore it, simply pipe it back into the `mysql` command:
|
|||
mysql -u <user> -p -h <db-host> <database> < vkunja-backup.sql
|
||||
{{< /highlight >}}
|
||||
|
||||
## PostgreSQL
|
||||
### PostgreSQL
|
||||
|
||||
To create a backup from PostgreSQL use the `pg_dump` command:
|
||||
|
||||
|
@ -49,6 +54,6 @@ psql -U <user> -h <db-host> <database> < vikunja-backup.sql
|
|||
|
||||
For more information, please visit the [relevant PostgreSQL documentation](https://www.postgresql.org/docs/12/backup-dump.html).
|
||||
|
||||
## SQLite
|
||||
### SQLite
|
||||
|
||||
To backup sqllite databases, it is enough to copy the database elsewhere.
|
||||
To back up sqllite databases, it is enough to copy the [database file]({{< ref "config.md" >}}#path) to somwhere else.
|
||||
|
|
|
@ -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"
|
||||
|
@ -10,20 +10,35 @@ menu:
|
|||
|
||||
# Build Vikunja from source
|
||||
|
||||
Vikunja being a go application, has no other dependencies than go itself.
|
||||
All libraries are bundeled inside the repo in the `vendor/` folder, so all it boils down to are these steps:
|
||||
To completely build Vikunja from source, you need to build the api and frontend.
|
||||
|
||||
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.9`.
|
||||
2. Make sure [Mage](https://magefile) is properly installed on your system.
|
||||
3. Clone the repo with `git clone https://code.vikunja.io/api`
|
||||
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.
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## API
|
||||
|
||||
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.
|
||||
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.
|
||||
When builing entirely with `mage`, you dont need to do this, `mage build:generate` will be run automatically when running `mage build:build`.
|
||||
|
||||
# Build for different architectures
|
||||
### Build for different architectures
|
||||
|
||||
To build for other platforms and architectures than the one you're currently on, simply run `mage release:release` or `mage release:{linux|windows|darwin}`.
|
||||
|
||||
More options are available, please refer to the [magefile docs]({{< ref "../development/mage.md">}}) for more details.
|
||||
More options are available, please refer to the [magefile docs]({{< ref "../development/mage.md">}}) for more details.
|
||||
|
||||
## Frontend
|
||||
|
||||
The code for the frontend is located at [code.vikunja.io/frontend](https://code.vikunja.io/frontend).
|
||||
|
||||
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.
|
||||
|
|
|
@ -10,8 +10,9 @@ menu:
|
|||
|
||||
# Configuration options
|
||||
|
||||
You can either use a `config.yml` file in the root directory of vikunja or set all config option with
|
||||
You can either use a `config.yml` file in the root directory of vikunja or set almost all config option with
|
||||
environment variables. If you have both, the value set in the config file is used.
|
||||
Right now it is not possible to configure openid authentication via environment variables.
|
||||
|
||||
Variables are nested in the `config.yml`, these nested variables become `VIKUNJA_FIRST_CHILD` when configuring via
|
||||
environment variables. So setting
|
||||
|
@ -76,7 +77,32 @@ Default: `<jwt-secret>`
|
|||
|
||||
Full path: `service.JWTSecret`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_JWT_SECRET`
|
||||
Environment path: `VIKUNJA_SERVICE_JWTSECRET`
|
||||
|
||||
|
||||
### jwtttl
|
||||
|
||||
The duration of the issed JWT tokens in seconds.
|
||||
The default is 259200 seconds (3 Days).
|
||||
|
||||
Default: `259200`
|
||||
|
||||
Full path: `service.jwtttl`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_JWTTTL`
|
||||
|
||||
|
||||
### jwtttllong
|
||||
|
||||
The duration of the "remember me" time in seconds. When the login request is made with
|
||||
the long param set, the token returned will be valid for this period.
|
||||
The default is 2592000 seconds (30 Days).
|
||||
|
||||
Default: `2592000`
|
||||
|
||||
Full path: `service.jwtttllong`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_JWTTTLLONG`
|
||||
|
||||
|
||||
### interface
|
||||
|
@ -136,6 +162,17 @@ Full path: `service.rootpath`
|
|||
Environment path: `VIKUNJA_SERVICE_ROOTPATH`
|
||||
|
||||
|
||||
### staticpath
|
||||
|
||||
Path on the file system to serve static files from. Set to the path of the frontend files to host frontend alongside the api.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `service.staticpath`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_STATICPATH`
|
||||
|
||||
|
||||
### maxitemsperpage
|
||||
|
||||
The max number of items which can be returned per page
|
||||
|
@ -285,6 +322,18 @@ Full path: `service.enableuserdeletion`
|
|||
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
|
||||
|
||||
|
||||
### maxavatarsize
|
||||
|
||||
The maximum size clients will be able to request for user avatars.
|
||||
If clients request a size bigger than this, it will be changed on the fly.
|
||||
|
||||
Default: `1024`
|
||||
|
||||
Full path: `service.maxavatarsize`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_MAXAVATARSIZE`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## database
|
||||
|
@ -315,7 +364,7 @@ Environment path: `VIKUNJA_DATABASE_USER`
|
|||
|
||||
### password
|
||||
|
||||
Databse password
|
||||
Database password
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
|
@ -326,7 +375,7 @@ Environment path: `VIKUNJA_DATABASE_PASSWORD`
|
|||
|
||||
### host
|
||||
|
||||
Databse host
|
||||
Database host
|
||||
|
||||
Default: `localhost`
|
||||
|
||||
|
@ -337,7 +386,7 @@ Environment path: `VIKUNJA_DATABASE_HOST`
|
|||
|
||||
### database
|
||||
|
||||
Databse to use
|
||||
Database to use
|
||||
|
||||
Default: `vikunja`
|
||||
|
||||
|
@ -402,6 +451,39 @@ Full path: `database.sslmode`
|
|||
Environment path: `VIKUNJA_DATABASE_SSLMODE`
|
||||
|
||||
|
||||
### sslcert
|
||||
|
||||
The path to the client cert. Only used with postgres.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `database.sslcert`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_SSLCERT`
|
||||
|
||||
|
||||
### sslkey
|
||||
|
||||
The path to the client key. Only used with postgres.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `database.sslkey`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_SSLKEY`
|
||||
|
||||
|
||||
### sslrootcert
|
||||
|
||||
The path to the ca cert. Only used with postgres.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `database.sslrootcert`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_SSLROOTCERT`
|
||||
|
||||
|
||||
### tls
|
||||
|
||||
Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
|
||||
|
@ -575,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`
|
||||
|
||||
|
@ -584,6 +667,17 @@ Full path: `mailer.port`
|
|||
Environment path: `VIKUNJA_MAILER_PORT`
|
||||
|
||||
|
||||
### authtype
|
||||
|
||||
SMTP Auth Type. Can be either `plain`, `login` or `cram-md5`.
|
||||
|
||||
Default: `plain`
|
||||
|
||||
Full path: `mailer.authtype`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_AUTHTYPE`
|
||||
|
||||
|
||||
### username
|
||||
|
||||
SMTP username
|
||||
|
@ -1080,3 +1174,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`
|
||||
|
||||
|
||||
|
|
|
@ -50,6 +50,8 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
|
|
|
@ -25,6 +25,9 @@ All examples on this page already reflect this and do not require additional wor
|
|||
|
||||
## Redis
|
||||
|
||||
While Vikunja has support to use redis as a caching backend, you'll probably not need it unless you're using Vikunja
|
||||
with more than a handful of users.
|
||||
|
||||
To use redis, you'll need to add this to the config examples below:
|
||||
|
||||
{{< highlight yaml >}}
|
||||
|
@ -44,6 +47,80 @@ services:
|
|||
image: redis
|
||||
{{< /highlight >}}
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Vikunja supports postgres, mysql and sqlite as a database backend. The examples on this page use mysql with a mariadb container.
|
||||
To use postgres as a database backend, change the `db` section of the examples to this:
|
||||
|
||||
{{< highlight yaml >}}
|
||||
db:
|
||||
image: postgres:13
|
||||
environment:
|
||||
POSTGRES_PASSWORD: secret
|
||||
POSTGRES_USER: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
{{< /highlight >}}
|
||||
|
||||
You'll also need to change the `VIKUNJA_DATABASE_TYPE` to `postgres` on the api container declaration.
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> The mariadb container can sometimes take a while to initialize, especially on the first run.
|
||||
During this time, the api container will fail to start at all. It will automatically restart every few seconds.
|
||||
</div>
|
||||
|
||||
## Example without any proxy
|
||||
|
||||
This example lets you host Vikunja without any reverse proxy in front of it. This is the absolute minimum configuration
|
||||
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)
|
||||
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.
|
||||
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: secret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
api:
|
||||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: http://<your public frontend url with slash>/
|
||||
ports:
|
||||
- 3456:3456
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
frontend:
|
||||
image: vikunja/frontend
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
VIKUNJA_API_URL: http://<your-ip-here>:3456/api/v1
|
||||
restart: unless-stopped
|
||||
{{< /highlight >}}
|
||||
|
||||
## Example with traefik 2
|
||||
|
||||
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/).
|
||||
|
@ -66,6 +143,8 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
networks:
|
||||
|
@ -124,6 +203,8 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
networks:
|
||||
|
@ -217,6 +298,8 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
|
@ -275,6 +358,8 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
|
@ -295,3 +380,83 @@ services:
|
|||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
{{< /highlight >}}
|
||||
|
||||
## Setup on a Synology NAS
|
||||
|
||||
There is a proxy preinstalled in DSM, so if you want to access vikunja from outside,
|
||||
you can prepare 2 proxy rules:
|
||||
|
||||
* a redirection rule for vikunja's api (see example screenshot using port 3456)
|
||||
* a similar redirection rule for vikunja's frontend (using port 4321)
|
||||
|
||||
![Synology Proxy Settings](/docs/synology-proxy-1.png)
|
||||
|
||||
You should also add 2 empty folders for mariadb and vikunja inside Synology's
|
||||
docker main folders:
|
||||
|
||||
* Docker
|
||||
* vikunja
|
||||
* mariadb
|
||||
|
||||
Synology has it's own GUI for managing Docker containers... But it's easier via docker compose.
|
||||
|
||||
To do that, you can
|
||||
|
||||
* either activate SSH and paste the adapted compose file in a terminal (using Putty or similar)
|
||||
* without activating SSH as a "custom script" (go to Control Panel / Task Scheduler / Create / Scheduled Task / User-defined script)
|
||||
* 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:
|
||||
|
||||
![Portainer Stack deploy](/docs/synology-proxy-2.png)
|
||||
|
||||
The docker-compose file we're going to use is very similar to the [example without any proxy](#example-without-any-proxy) above:
|
||||
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: secret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
api:
|
||||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <a super secure random secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
|
||||
ports:
|
||||
- 3456:3456
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
frontend:
|
||||
image: vikunja/frontend
|
||||
ports:
|
||||
- 4321:80
|
||||
environment:
|
||||
VIKUNJA_API_URL: http://vikunja-api-domain.tld/api/v1
|
||||
restart: unless-stopped
|
||||
{{< /highlight >}}
|
||||
|
||||
You may want to change the volumes to match the rest of your setup.
|
||||
|
||||
Once deployed, you might want to change the [`PUID` and `GUID` settings]({{< ref "install-backend.md">}}#setting-user-and-group-id-of-the-user-running-vikunja) or [set the time zone]({{< ref "config.md">}}#timezone).
|
||||
|
||||
After registering all your users, you might also want to [disable the user registration]({{<ref "config.md">}}#enableregistration).
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ ln -s /opt/vikunja/vikunja /usr/bin/vikunja
|
|||
|
||||
### Systemd service
|
||||
|
||||
Take the following `service` file and adapt it to your needs:
|
||||
Save the following service file to `/etc/systemd/system/vikunja.service` and adapt it to your needs:
|
||||
|
||||
{{< highlight service >}}
|
||||
[Unit]
|
||||
|
@ -83,8 +83,6 @@ WantedBy=multi-user.target
|
|||
|
||||
If you've installed Vikunja to a directory other than `/opt/vikunja`, you need to adapt `WorkingDirectory` accordingly.
|
||||
|
||||
Save the file to `/etc/systemd/system/vikunja.service`
|
||||
|
||||
After you made all nessecary modifications, it's time to start the service:
|
||||
|
||||
{{< highlight bash >}}
|
||||
|
@ -151,6 +149,7 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <generated secret>
|
||||
VIKUNJA_SERVICE_FRONTENDURL: https://<your public frontend url with slash>/
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
db:
|
||||
|
|
|
@ -11,16 +11,20 @@ menu:
|
|||
|
||||
# Installing
|
||||
|
||||
Vikunja consists of two parts: [Backend](https://code.vikunja.io/api) and [frontend](https://code.vikunja.io/frontend).
|
||||
While the backend is required, the frontend is not.
|
||||
You don't neccesarily need to have a web-frontend, using Vikunja via the [mobile app](https://code.vikunja.io/app) is totally fine.
|
||||
Vikunja consists of two parts: [API](https://code.vikunja.io/api) and [frontend](https://code.vikunja.io/frontend).
|
||||
|
||||
However, using the web frontend is highly reccommended.
|
||||
You will always need to install at least the API.
|
||||
To actually use Vikunja you'll also need to somehow install a frontend to use it.
|
||||
You can either:
|
||||
|
||||
Vikunja can be installed in various forms.
|
||||
* [Install the web frontend]({{< ref "install-frontend.md">}})
|
||||
* Use the desktop app, which is essentially a web frontend packaged for easy installation on desktop devices
|
||||
* Use the mobile app only, but as of right now it only supports the very basic features of Vikunja
|
||||
|
||||
Vikunja can be installed in various ways.
|
||||
This document provides an overview and instructions for the different methods.
|
||||
|
||||
* [Backend]({{< ref "install-backend.md">}})
|
||||
* [API]({{< ref "install-backend.md">}})
|
||||
* [Installing from binary]({{< ref "install-backend.md#install-from-binary">}})
|
||||
* [Verify the GPG signature]({{< ref "install-backend.md#verify-the-gpg-signature">}})
|
||||
* [Set it up]({{< ref "install-backend.md#set-it-up">}})
|
||||
|
@ -49,3 +53,6 @@ A third-party Helm Chart is available from the k8s-at-home project [here](https:
|
|||
* [Docker Compose is MUCH Easier Than you Think - Let's Install Vikunja](https://www.youtube.com/watch?v=fGlz2PkXjuo) (Youtube)
|
||||
* [Setup Vikunja using Docker Compose - Homelab Wiki](https://thehomelab.wiki/books/docker/page/setup-vikunja-using-docker-compose)
|
||||
* [A Closer look at Vikunja - Email Notifications - Enable or Disable Registrations - Allow Attachments](https://www.youtube.com/watch?v=47wj9pRT6Gw) (Youtube)
|
||||
* [Install Vikunja in Docker for self-hosted Task Tracking](https://smarthomepursuits.com/install-vikunja-in-docker-for-self-hosted-task-tracking/)
|
||||
* [Self-Hosted To-Do List with Vikunja in Docker](https://www.youtube.com/watch?v=DqyqDWpEvKI) (Youtube)
|
||||
* [Vikunja self-hosted (step by step)](https://nguyenminhhung.com/vikunja-self-hosted-step-by-step/)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: "Hosting Vikunja with k8s"
|
||||
date: 2022-08-12T13:41:48+02:00
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
There are two third-party Helm-Charts which can be used to host Vikunja with k8s:
|
||||
|
||||
* [Truecharts](https://truecharts.org/docs/charts/stable/vikunja/)
|
||||
* [k8s at Home](https://github.com/k8s-at-home/charts)
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
date: "2022-08-09:00:00+02:00"
|
||||
title: "OpenID example configurations"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# OpenID example configurations
|
||||
|
||||
On this page you will find examples about how to set up Vikunja with a third-party OpenID provider.
|
||||
To add another example, please [edit this document](https://kolaente.dev/vikunja/api/src/branch/main/docs/content/doc/setup/openid-examples.md) and send a PR.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Authelia
|
||||
|
||||
Vikunja Config:
|
||||
|
||||
```yaml
|
||||
openid:
|
||||
enabled: true
|
||||
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
|
||||
providers:
|
||||
- name: Authelia
|
||||
authurl: https://login.mydomain.com
|
||||
clientid: <vikunja-id>
|
||||
clientsecret: <vikunja secret>
|
||||
```
|
||||
|
||||
Authelia config:
|
||||
|
||||
```yaml
|
||||
- id: <vikunja-id>
|
||||
description: Vikunja
|
||||
secret: <vikunja secret>
|
||||
redirect_uris:
|
||||
- https://vikunja.mydomain.com/auth/openid/authelia
|
||||
scopes:
|
||||
- openid
|
||||
- email
|
||||
- profile
|
||||
```
|
|
@ -80,6 +80,22 @@ server {
|
|||
<b>NOTE:</b> If you change the max upload size in Vikunja's settings, you'll need to also change the <code>client_max_body_size</code> in the nginx proxy config.
|
||||
</div>
|
||||
|
||||
## NGINX Proxy Manager (NPM)
|
||||
|
||||
1. Create a standard Proxy Host for the Vikunja Frontend within NPM and point it to the URL you plan to use. The next several steps will enable the Proxy Host to successfully navigate to the API (on port 3456).
|
||||
2. Verify that the page will pull up in your browser. (Do not bother trying to log in. It won't work. Trust me.)
|
||||
3. Now, we'll work with the NPM container, so you need to identify the container name for your NPM installation. e.g. NGINX-PM
|
||||
4. From the command line, enter `sudo docker exec -it [NGINX-PM container name] /bin/bash` and navigate to the proxy hosts folder where the `.conf` files are stashed. Probably `/data/nginx/proxy_host`. (This folder is a persistent folder created in the NPM container and mounted by NPM.)
|
||||
5. Locate the `.conf` file where the server_name inside the file matches your Vikunja Proxy Host. Once found, add the following code, unchanged, just above the existing location block in that file. (They are listed by number, not name.)
|
||||
```
|
||||
location ~* ^/(api|dav|\.well-known)/ {
|
||||
proxy_pass http://api:3456;
|
||||
client_max_body_size 20M;
|
||||
}
|
||||
```
|
||||
6. After saving the edited file, return to NPM's UI browser window and refresh the page to verify your Proxy Host for Vikunja is still online.
|
||||
7. Now, switch over to your Vikunja browswer window and hit refresh. If you configured your URL correctly in original Vikunja container, you should be all set and the browser will correctly show Vikunja. If not, you'll need to adjust the address in the top of the login subscreen to match your proxy address.
|
||||
|
||||
## Apache
|
||||
|
||||
Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
|
||||
|
@ -108,4 +124,4 @@ Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
|
|||
|
||||
**Note:** The apache modules `proxy`, `proxy_http` and `rewrite` must be enabled for this.
|
||||
|
||||
For more details see the [frontend apache configuration]({{< ref "install-frontend.md#apache">}}).
|
||||
For more details see the [frontend apache configuration]({{< ref "install-frontend.md#apache">}}).
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
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.
|
||||
|
||||
Then, run
|
||||
|
||||
```
|
||||
pnpm vite build --base=/SUBPATH
|
||||
pnpm workbox copyLibraries dist/
|
||||
```
|
||||
|
||||
Where `SUBPATH` is the subdirectory you want to run Vikunja on.
|
||||
|
||||
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.
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
date: "2022-07-07:00:00+02:00"
|
||||
title: "Versions"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Vikunja Versions
|
||||
|
||||
The Vikunja api and frontend are available in two different release flavors.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Stable
|
||||
|
||||
Stable releases have a fixed version number like `0.18.2` and are published at irregular intervals whenever a new version is ready.
|
||||
They receive few bugfixes and security patches.
|
||||
|
||||
We use [Semantic Versioning](#) for these releases.
|
||||
|
||||
## Unstable
|
||||
|
||||
Unstable versions are build every time a PR is merged or a commit to the main development branch is made.
|
||||
As such, they contain the current development code and are more likely to have bugs.
|
||||
There might be multiple new such builds a day.
|
||||
|
||||
Versions contain the last stable version, the number of commits since then and the commit the currently running binary was built from.
|
||||
They look like this: `v0.18.1+269-5cc4927b9e`
|
||||
|
||||
The demo instance at [try.vikunja.io](https://try.vikunja.io) automatically updates and always runs the last unstable build.
|
||||
|
||||
## Switching between versions
|
||||
|
||||
First you should create a backup of your current setup!
|
||||
|
||||
Switching between versions is the same process as [upgrading]({{< ref install-backend.md >}}#updating).
|
||||
Simply replace the stable binary with an unstable one or vice-versa.
|
||||
|
||||
For installations using docker, it is as simple as using the `unstable` or `latest` tag to switch between versions.
|
||||
|
||||
**Note:** While switching from stable to unstable should work without any problem, switching back might work but is not recommended and might break your instance.
|
||||
To switch from unstable back to stable the best way is to wait for the next stable release after the used unstable build and then upgrade to that.
|
|
@ -11,7 +11,7 @@ menu:
|
|||
# API Documentation
|
||||
|
||||
You can find the api docs under `http://vikunja.tld/api/v1/docs` of your instance.
|
||||
A public instance is available on [try.vikunja.io](http://try.vikunja.io/api/v1/docs).
|
||||
A public instance is available on [try.vikunja.io](https://try.vikunja.io/api/v1/docs).
|
||||
|
||||
These docs are autgenerated from annotations in the code with swagger.
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ title: "Errors"
|
|||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Errors
|
||||
|
@ -52,14 +52,16 @@ This document describes the different errors Vikunja can return.
|
|||
|
||||
## List
|
||||
|
||||
| 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. |
|
||||
| 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". |
|
||||
| 3010 | 412 | The list must belong to a namespace. |
|
||||
|
||||
## Task
|
||||
|
||||
|
@ -80,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
|
||||
|
||||
|
|
|
@ -18,4 +18,8 @@ server {
|
|||
location /docs/contact {
|
||||
return 301 $scheme://vikunja.io/en/contact;
|
||||
}
|
||||
|
||||
location /docs/docs {
|
||||
return 301 $scheme://vikunja.io/docs;
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 121 KiB |
Binary file not shown.
After Width: | Height: | Size: 502 KiB |
159
go.mod
159
go.mod
|
@ -20,72 +20,135 @@ 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/adlio/trello v1.9.0
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
|
||||
github.com/adlio/trello v1.10.0
|
||||
github.com/arran4/golang-ical v0.0.0-20220517104411-fd89fefb0182
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/c2h5oh/datasize v0.0.0-20200825124411-48ed595a09d2
|
||||
github.com/coreos/go-oidc/v3 v3.0.0
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
|
||||
github.com/coreos/go-oidc/v3 v3.4.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.3.1
|
||||
github.com/getsentry/sentry-go v0.11.0
|
||||
github.com/go-errors/errors v1.1.1 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.3
|
||||
github.com/gabriel-vasile/mimetype v1.4.1
|
||||
github.com/getsentry/sentry-go v0.14.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.6.1
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/labstack/echo/v4 v4.5.0
|
||||
github.com/labstack/gommon v0.3.0
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
|
||||
github.com/lib/pq v1.10.3
|
||||
github.com/magefile/mage v1.11.0
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.8
|
||||
github.com/imdario/mergo v0.3.13
|
||||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/labstack/echo/v4 v4.9.1
|
||||
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.11.0
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.6.0
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/swaggo/swag v1.7.1
|
||||
github.com/ulule/limiter/v3 v3.8.0
|
||||
github.com/yuin/goldmark v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
github.com/spf13/afero v1.9.2
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.13.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/swaggo/swag v1.8.7
|
||||
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.3.4
|
||||
github.com/yuin/goldmark v1.5.2
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/image v0.1.0
|
||||
golang.org/x/oauth2 v0.1.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/term v0.1.0
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
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
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
src.techknowlogick.com/xgo v1.5.1-0.20220906164532-735bfdfb90d9
|
||||
src.techknowlogick.com/xormigrate v1.5.0
|
||||
xorm.io/builder v0.3.12
|
||||
xorm.io/xorm v1.3.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
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/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/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-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/hcl 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.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // 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.5 // 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/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.4.1 // 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.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/adlio/trello => github.com/kolaente/trello v1.7.1-0.20201216234312-5c4ef79b531e
|
||||
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
|
||||
)
|
||||
|
||||
go 1.15
|
||||
go 1.19
|
||||
|
|
52
magefile.go
52
magefile.go
|
@ -14,6 +14,7 @@
|
|||
// 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/>.
|
||||
|
||||
//go:build mage
|
||||
// +build mage
|
||||
|
||||
package main
|
||||
|
@ -26,7 +27,6 @@ import (
|
|||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
@ -74,9 +74,21 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
func runCmdWithOutput(name string, arg ...string) (output []byte, err error) {
|
||||
cmd := exec.Command(name, arg...)
|
||||
output, err = cmd.Output()
|
||||
if err != nil {
|
||||
if ee, is := err.(*exec.ExitError); is {
|
||||
return nil, fmt.Errorf("error running command: %s, %s", string(ee.Stderr), err)
|
||||
}
|
||||
return nil, fmt.Errorf("error running command: %s", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func setVersion() {
|
||||
versionCmd := exec.Command("git", "describe", "--tags", "--always", "--abbrev=10")
|
||||
version, err := versionCmd.Output()
|
||||
version, err := runCmdWithOutput("git", "describe", "--tags", "--always", "--abbrev=10")
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting version: %s\n", err)
|
||||
os.Exit(1)
|
||||
|
@ -117,8 +129,7 @@ func setExecutable() {
|
|||
}
|
||||
|
||||
func setApiPackages() {
|
||||
cmd := exec.Command("go", "list", "all")
|
||||
pkgs, err := cmd.Output()
|
||||
pkgs, err := runCmdWithOutput("go", "list", "all")
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting packages: %s\n", err)
|
||||
os.Exit(1)
|
||||
|
@ -145,8 +156,7 @@ func setRootPath() {
|
|||
|
||||
func setGoFiles() {
|
||||
// GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
|
||||
cmd := exec.Command("find", ".", "-name", "*.go", "-type", "f", "!", "-path", "*/bindata.go")
|
||||
files, err := cmd.Output()
|
||||
files, err := runCmdWithOutput("find", ".", "-name", "*.go", "-type", "f", "!", "-path", "*/bindata.go")
|
||||
if err != nil {
|
||||
fmt.Printf("Error getting go files: %s\n", err)
|
||||
os.Exit(1)
|
||||
|
@ -170,7 +180,6 @@ func initVars() {
|
|||
setVersion()
|
||||
setBinLocation()
|
||||
setPkgVersion()
|
||||
setApiPackages()
|
||||
setGoFiles()
|
||||
Ldflags = `-X "` + PACKAGE + `/pkg/version.Version=` + VersionNumber + `" -X "main.Tags=` + Tags + `"`
|
||||
}
|
||||
|
@ -340,8 +349,9 @@ type Test mg.Namespace
|
|||
// Runs all tests except integration tests
|
||||
func (Test) Unit() {
|
||||
mg.Deps(initVars)
|
||||
setApiPackages()
|
||||
// We run everything sequentially and not in parallel to prevent issues with real test databases
|
||||
args := append([]string{"test", Goflags[0], "-p", "1", "-timeout", "20m"}, ApiPackages...)
|
||||
args := append([]string{"test", Goflags[0], "-p", "1", "-coverprofile", "cover.out", "-timeout", "45m"}, ApiPackages...)
|
||||
runAndStreamOutput("go", args...)
|
||||
}
|
||||
|
||||
|
@ -356,7 +366,7 @@ func (Test) Coverage() {
|
|||
func (Test) Integration() {
|
||||
mg.Deps(initVars)
|
||||
// We run everything sequentially and not in parallel to prevent issues with real test databases
|
||||
runAndStreamOutput("go", "test", Goflags[0], "-p", "1", "-timeout", "20m", PACKAGE+"/pkg/integrations")
|
||||
runAndStreamOutput("go", "test", Goflags[0], "-p", "1", "-timeout", "45m", PACKAGE+"/pkg/integrations")
|
||||
}
|
||||
|
||||
type Check mg.Namespace
|
||||
|
@ -395,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.31.0")
|
||||
fmt.Println("curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.47.3")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
@ -548,7 +558,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
|
||||
}
|
||||
|
||||
|
@ -652,7 +664,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,14 +700,14 @@ func (Release) Packages() error {
|
|||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -708,7 +720,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
|
||||
|
@ -872,7 +884,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
|
||||
}
|
||||
|
@ -1042,7 +1054,7 @@ func printConfig(config []*configOption, level int, parent string) (rendered str
|
|||
fullPath := parent + "." + option.key
|
||||
|
||||
rendered += "Full path: `" + fullPath + "`\n\n"
|
||||
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(fullPath) + "`\n\n"
|
||||
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(strings.ToUpper(fullPath)) + "`\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1061,7 +1073,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
|
||||
}
|
||||
|
@ -1111,7 +1123,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
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
package caldav
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -150,6 +149,15 @@ END:VCALENDAR` // Need a line break
|
|||
return
|
||||
}
|
||||
|
||||
func formatDuration(duration time.Duration) string {
|
||||
seconds := duration.Seconds() - duration.Minutes()*60
|
||||
minutes := duration.Minutes() - duration.Hours()*60
|
||||
|
||||
return strconv.FormatFloat(duration.Hours(), 'f', 0, 64) + `H` +
|
||||
strconv.FormatFloat(minutes, 'f', 0, 64) + `M` +
|
||||
strconv.FormatFloat(seconds, 'f', 0, 64) + `S`
|
||||
}
|
||||
|
||||
// ParseTodos returns a caldav vcalendar string with todos
|
||||
func ParseTodos(config *Config, todos []*Todo) (caldavtodos string) {
|
||||
caldavtodos = `BEGIN:VCALENDAR
|
||||
|
@ -172,11 +180,15 @@ SUMMARY:` + t.Summary + getCaldavColor(t.Color)
|
|||
|
||||
if t.Start.Unix() > 0 {
|
||||
caldavtodos += `
|
||||
DTSTART: ` + makeCalDavTimeFromTimeStamp(t.Start)
|
||||
DTSTART:` + makeCalDavTimeFromTimeStamp(t.Start)
|
||||
if t.Duration != 0 && t.DueDate.Unix() == 0 {
|
||||
caldavtodos += `
|
||||
DURATION:PT` + formatDuration(t.Duration)
|
||||
}
|
||||
}
|
||||
if t.End.Unix() > 0 {
|
||||
caldavtodos += `
|
||||
DTEND: ` + makeCalDavTimeFromTimeStamp(t.End)
|
||||
DTEND:` + makeCalDavTimeFromTimeStamp(t.End)
|
||||
}
|
||||
if t.Description != "" {
|
||||
re := regexp.MustCompile(`\r?\n`)
|
||||
|
@ -209,11 +221,6 @@ DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
|
|||
CREATED:` + makeCalDavTimeFromTimeStamp(t.Created)
|
||||
}
|
||||
|
||||
if t.Duration != 0 {
|
||||
caldavtodos += `
|
||||
DURATION:PT` + fmt.Sprintf("%.6f", t.Duration.Hours()) + `H` + fmt.Sprintf("%.6f", t.Duration.Minutes()) + `M` + fmt.Sprintf("%.6f", t.Duration.Seconds()) + `S`
|
||||
}
|
||||
|
||||
if t.Priority != 0 {
|
||||
caldavtodos += `
|
||||
PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority))
|
||||
|
|
|
@ -23,7 +23,8 @@ import (
|
|||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/laurent22/ical-go"
|
||||
|
||||
ics "github.com/arran4/golang-ical"
|
||||
)
|
||||
|
||||
func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.TaskWithComments) string {
|
||||
|
@ -60,21 +61,15 @@ func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*m
|
|||
}
|
||||
|
||||
func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
||||
parsed, err := ical.ParseCalendar(content)
|
||||
parsed, err := ics.ParseCalendar(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We put the task details in a map to be able to handle them more easily
|
||||
task := make(map[string]string)
|
||||
for _, c := range parsed.Children {
|
||||
if c.Name == "VTODO" {
|
||||
for _, entry := range c.Children {
|
||||
task[entry.Name] = entry.Value
|
||||
}
|
||||
// Breaking, to only process the first task
|
||||
break
|
||||
}
|
||||
for _, c := range parsed.Components[0].UnknownPropertiesIANAProperties() {
|
||||
task[c.IANAToken] = c.Value
|
||||
}
|
||||
|
||||
// Parse the priority
|
||||
|
@ -91,10 +86,13 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
// Parse the enddate
|
||||
duration, _ := time.ParseDuration(task["DURATION"])
|
||||
|
||||
description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",")
|
||||
description = strings.ReplaceAll(description, "\\n", "\n")
|
||||
|
||||
vTask = &models.Task{
|
||||
UID: task["UID"],
|
||||
Title: task["SUMMARY"],
|
||||
Description: task["DESCRIPTION"],
|
||||
Description: description,
|
||||
Priority: priority,
|
||||
DueDate: caldavTimeToTimestamp(task["DUE"]),
|
||||
Updated: caldavTimeToTimestamp(task["DTSTAMP"]),
|
||||
|
@ -125,6 +123,10 @@ func caldavTimeToTimestamp(tstring string) time.Time {
|
|||
format = `20060102T150405Z`
|
||||
}
|
||||
|
||||
if len(tstring) == 8 {
|
||||
format = `20060102`
|
||||
}
|
||||
|
||||
t, err := time.Parse(format, tstring)
|
||||
if err != nil {
|
||||
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err)
|
||||
|
|
|
@ -24,6 +24,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/initialize"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
@ -103,7 +105,7 @@ func getUserFromArg(s *xorm.Session, arg string) *user.User {
|
|||
log.Fatalf("Invalid user id: %s", err)
|
||||
}
|
||||
|
||||
u, err := user.GetUserByID(s, id)
|
||||
u, err := user.GetUserWithEmail(s, &user.User{ID: id})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not get user: %s", err)
|
||||
}
|
||||
|
@ -175,6 +177,11 @@ var userCreateCmd = &cobra.Command{
|
|||
Email: userFlagEmail,
|
||||
Password: getPasswordFromFlagOrInput(),
|
||||
}
|
||||
|
||||
if !govalidator.IsEmail(userFlagEmail) {
|
||||
log.Fatalf("Provided email is invalid.")
|
||||
}
|
||||
|
||||
newUser, err := user.CreateUser(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
|
|
|
@ -21,8 +21,10 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata" // Imports time zone data instead of relying on the os
|
||||
|
@ -37,14 +39,17 @@ type Key string
|
|||
const (
|
||||
// #nosec
|
||||
ServiceJWTSecret Key = `service.JWTSecret`
|
||||
ServiceJWTTTL Key = `service.jwtttl`
|
||||
ServiceJWTTTLLong Key = `service.jwtttllong`
|
||||
ServiceInterface Key = `service.interface`
|
||||
ServiceUnixSocket Key = `service.unixsocket`
|
||||
ServiceUnixSocketMode Key = `service.unixsocketmode`
|
||||
ServiceFrontendurl Key = `service.frontendurl`
|
||||
ServiceEnableCaldav Key = `service.enablecaldav`
|
||||
ServiceRootpath Key = `service.rootpath`
|
||||
ServiceStaticpath Key = `service.staticpath`
|
||||
ServiceMaxItemsPerPage Key = `service.maxitemsperpage`
|
||||
// Deprecated. Use metrics.enabled
|
||||
// Deprecated: Use metrics.enabled
|
||||
ServiceEnableMetrics Key = `service.enablemetrics`
|
||||
ServiceMotd Key = `service.motd`
|
||||
ServiceEnableLinkSharing Key = `service.enablelinksharing`
|
||||
|
@ -57,6 +62,7 @@ const (
|
|||
ServiceTestingtoken Key = `service.testingtoken`
|
||||
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
||||
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
||||
ServiceMaxAvatarSize Key = `service.maxavatarsize`
|
||||
|
||||
AuthLocalEnabled Key = `auth.local.enabled`
|
||||
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
||||
|
@ -76,6 +82,9 @@ const (
|
|||
DatabaseMaxIdleConnections Key = `database.maxidleconnections`
|
||||
DatabaseMaxConnectionLifetime Key = `database.maxconnectionlifetime`
|
||||
DatabaseSslMode Key = `database.sslmode`
|
||||
DatabaseSslCert Key = `database.sslcert`
|
||||
DatabaseSslKey Key = `database.sslkey`
|
||||
DatabaseSslRootCert Key = `database.sslrootcert`
|
||||
DatabaseTLS Key = `database.tls`
|
||||
|
||||
CacheEnabled Key = `cache.enabled`
|
||||
|
@ -87,6 +96,7 @@ const (
|
|||
MailerPort Key = `mailer.port`
|
||||
MailerUsername Key = `mailer.username`
|
||||
MailerPassword Key = `mailer.password`
|
||||
MailerAuthType Key = `mailer.authtype`
|
||||
MailerSkipTLSVerify Key = `mailer.skiptlsverify`
|
||||
MailerFromEmail Key = `mailer.fromemail`
|
||||
MailerQueuelength Key = `mailer.queuelength`
|
||||
|
@ -151,6 +161,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
|
||||
|
@ -215,6 +237,39 @@ func (k Key) setDefault(i interface{}) {
|
|||
viper.SetDefault(string(k), i)
|
||||
}
|
||||
|
||||
// Tries different methods to figure out the binary folder.
|
||||
// Copied and adopted from https://github.com/speedata/publisher/commit/3b668668d57edef04ea854d5bbd58f83eb1b799f
|
||||
func getBinaryDirLocation() string {
|
||||
// First, check if the standard library gives us the path. This will work 99% of the time.
|
||||
ex, err := os.Executable()
|
||||
if err == nil {
|
||||
return filepath.Dir(ex)
|
||||
}
|
||||
|
||||
// Then check if the binary was run with a full path and use that if that's the case.
|
||||
if strings.Contains(os.Args[0], "/") {
|
||||
binDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return binDir
|
||||
}
|
||||
|
||||
exeSuffix := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
exeSuffix = ".exe"
|
||||
}
|
||||
|
||||
// All else failing, search for a vikunja binary in the current $PATH.
|
||||
// This can give wrong results.
|
||||
exeLocation, err := exec.LookPath("vikunja" + exeSuffix)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return filepath.Dir(exeLocation)
|
||||
}
|
||||
|
||||
// InitDefaultConfig sets default config values
|
||||
// This is an extra function so we can call it when initializing tests without initializing the full config
|
||||
func InitDefaultConfig() {
|
||||
|
@ -226,17 +281,15 @@ func InitDefaultConfig() {
|
|||
|
||||
// Service
|
||||
ServiceJWTSecret.setDefault(random)
|
||||
ServiceJWTTTL.setDefault(259200) // 72 hours
|
||||
ServiceJWTTTLLong.setDefault(2592000) // 30 days
|
||||
ServiceInterface.setDefault(":3456")
|
||||
ServiceUnixSocket.setDefault("")
|
||||
ServiceFrontendurl.setDefault("")
|
||||
ServiceEnableCaldav.setDefault(true)
|
||||
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
exPath := filepath.Dir(ex)
|
||||
ServiceRootpath.setDefault(exPath)
|
||||
ServiceRootpath.setDefault(getBinaryDirLocation())
|
||||
ServiceStaticpath.setDefault("")
|
||||
ServiceMaxItemsPerPage.setDefault(50)
|
||||
ServiceEnableMetrics.setDefault(false)
|
||||
ServiceMotd.setDefault("")
|
||||
|
@ -248,6 +301,7 @@ func InitDefaultConfig() {
|
|||
ServiceEnableTotp.setDefault(true)
|
||||
ServiceEnableEmailReminders.setDefault(true)
|
||||
ServiceEnableUserDeletion.setDefault(true)
|
||||
ServiceMaxAvatarSize.setDefault(1024)
|
||||
|
||||
// Auth
|
||||
AuthLocalEnabled.setDefault(true)
|
||||
|
@ -264,6 +318,9 @@ func InitDefaultConfig() {
|
|||
DatabaseMaxIdleConnections.setDefault(50)
|
||||
DatabaseMaxConnectionLifetime.setDefault(10000)
|
||||
DatabaseSslMode.setDefault("disable")
|
||||
DatabaseSslCert.setDefault("")
|
||||
DatabaseSslKey.setDefault("")
|
||||
DatabaseSslRootCert.setDefault("")
|
||||
DatabaseTLS.setDefault("false")
|
||||
|
||||
// Cacher
|
||||
|
@ -274,13 +331,14 @@ func InitDefaultConfig() {
|
|||
MailerEnabled.setDefault(false)
|
||||
MailerHost.setDefault("")
|
||||
MailerPort.setDefault("587")
|
||||
MailerUsername.setDefault("user")
|
||||
MailerUsername.setDefault("")
|
||||
MailerPassword.setDefault("")
|
||||
MailerSkipTLSVerify.setDefault(false)
|
||||
MailerFromEmail.setDefault("mail@vikunja")
|
||||
MailerQueuelength.setDefault(100)
|
||||
MailerQueueTimeout.setDefault(30)
|
||||
MailerForceSSL.setDefault(false)
|
||||
MailerAuthType.setDefault("plain")
|
||||
// Redis
|
||||
RedisEnabled.setDefault(false)
|
||||
RedisHost.setDefault("localhost:6379")
|
||||
|
@ -325,6 +383,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.
|
||||
|
@ -351,11 +413,17 @@ func InitConfig() {
|
|||
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName("config")
|
||||
|
||||
err = viper.ReadInConfig()
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
log.Println("Using default config.")
|
||||
return
|
||||
if viper.ConfigFileUsed() != "" {
|
||||
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
||||
|
||||
if err != nil {
|
||||
log.Println(err.Error())
|
||||
log.Println("Using default config.")
|
||||
}
|
||||
} else {
|
||||
log.Println("No config file found, using default or config from environment variables.")
|
||||
}
|
||||
|
||||
if CacheType.GetString() == "keyvalue" {
|
||||
|
@ -386,12 +454,14 @@ 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)
|
||||
}
|
||||
|
||||
log.Printf("Using config file: %s", viper.ConfigFileUsed())
|
||||
}
|
||||
|
||||
func random(length int) (string, error) {
|
||||
|
|
17
pkg/db/db.go
17
pkg/db/db.go
|
@ -19,7 +19,6 @@ package db
|
|||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -28,9 +27,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.
|
||||
|
@ -81,7 +80,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)
|
||||
|
||||
|
@ -150,13 +149,17 @@ func parsePostgreSQLHostPort(info string) (string, string) {
|
|||
|
||||
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",
|
||||
// postgresql://username:password@host:port/dbname[?paramspec]
|
||||
connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s&sslcert=%s&sslkey=%s&sslrootcert=%s",
|
||||
config.DatabaseUser.GetString(),
|
||||
config.DatabasePassword.GetString(),
|
||||
host,
|
||||
port,
|
||||
url.PathEscape(config.DatabaseUser.GetString()),
|
||||
url.PathEscape(config.DatabasePassword.GetString()),
|
||||
config.DatabaseDatabase.GetString(),
|
||||
config.DatabaseSslMode.GetString(),
|
||||
config.DatabaseSslCert.GetString(),
|
||||
config.DatabaseSslKey.GetString(),
|
||||
config.DatabaseSslRootCert.GetString(),
|
||||
)
|
||||
|
||||
engine, err = xorm.NewEngine("postgres", connStr)
|
||||
|
@ -186,7 +189,7 @@ func initSqliteEngine() (engine *xorm.Engine, err error) {
|
|||
}
|
||||
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open database file [uid=%d, gid=%d]: %s", os.Getuid(), os.Getgid(), err)
|
||||
return nil, fmt.Errorf("could not open database file [uid=%d, gid=%d]: %w", os.Getuid(), os.Getgid(), err)
|
||||
}
|
||||
_ = file.Close() // We directly close the file because we only want to check if it is writable. It will be reopened lazily later by xorm.
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ package db
|
|||
import (
|
||||
"encoding/json"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
|
@ -57,6 +59,15 @@ func Restore(table string, contents []map[string]interface{}) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if Type() == schemas.POSTGRES {
|
||||
idSequence := table + "_id_seq"
|
||||
_, err = x.Query("SELECT setval('" + idSequence + "', COALESCE(MAX(id), 1) )")
|
||||
if err != nil {
|
||||
log.Warningf("Could not reset id sequence for %s: %s", idSequence, err)
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -23,9 +23,10 @@ import (
|
|||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/names"
|
||||
)
|
||||
|
||||
// CreateTestEngine creates an instance of the db engine which lives in memory
|
||||
|
@ -48,7 +49,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)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
@ -136,9 +137,10 @@ func (f *File) Delete() (err error) {
|
|||
|
||||
err = afs.Remove(f.getFileName())
|
||||
if err != nil {
|
||||
if e, is := err.(*os.PathError); is {
|
||||
var perr *os.PathError
|
||||
if errors.As(err, &perr) {
|
||||
// Don't fail when removing the file failed
|
||||
log.Errorf("Error deleting file %d: %s", e.Error())
|
||||
log.Errorf("Error deleting file %d: %w", err)
|
||||
return s.Commit()
|
||||
}
|
||||
|
||||
|
|
|
@ -78,15 +78,15 @@ func FullInit() {
|
|||
|
||||
LightInit()
|
||||
|
||||
// Initialize the files handler
|
||||
files.InitFileHandler()
|
||||
|
||||
// Run the migrations
|
||||
migration.Migrate(nil)
|
||||
|
||||
// Set Engine
|
||||
InitEngines()
|
||||
|
||||
// Initialize the files handler
|
||||
files.InitFileHandler()
|
||||
|
||||
// Start the mail daemon
|
||||
mail.StartMailDaemon()
|
||||
|
||||
|
|
|
@ -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,33 @@
|
|||
// 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"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHealthcheck(t *testing.T) {
|
||||
t.Run("healthcheck", func(t *testing.T) {
|
||||
rec, err := newTestRequest(t, http.MethodGet, routes.HealthcheckHandler, ``, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), "OK")
|
||||
})
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package integrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -119,7 +120,7 @@ func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context)
|
|||
|
||||
func addUserTokenToContext(t *testing.T, user *user.User, c echo.Context) {
|
||||
// Get the token as a string
|
||||
token, err := auth.NewUserJWTAuthtoken(user)
|
||||
token, err := auth.NewUserJWTAuthtoken(user, false)
|
||||
assert.NoError(t, err)
|
||||
// We send the string token through the parsing function to get a valid jwt.Token
|
||||
tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
|
@ -174,8 +175,8 @@ func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) {
|
|||
t.Error("Error is nil")
|
||||
t.FailNow()
|
||||
}
|
||||
httperr, ok := err.(*echo.HTTPError)
|
||||
if !ok {
|
||||
var httperr *echo.HTTPError
|
||||
if !errors.As(err, &httperr) {
|
||||
t.Error("Error is not *echo.HTTPError")
|
||||
t.FailNow()
|
||||
}
|
||||
|
|
|
@ -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"}},{"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,"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-4","index":4,"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":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"}}]`)
|
||||
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"}},{"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,"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-4","index":4,"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":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"}}]`)
|
||||
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"}},{"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"}}`)
|
||||
})
|
||||
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"}},{"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"}}`)
|
||||
})
|
||||
t.Run("by due_date without suffix", func(t *testing.T) {
|
||||
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"}},{"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"}}`)
|
||||
})
|
||||
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"}},{"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"}}`)
|
||||
})
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
|
@ -244,12 +244,37 @@ func TestTaskCollection(t *testing.T) {
|
|||
// the current date.
|
||||
assert.Equal(t, "[]\n", rec.Body.String())
|
||||
})
|
||||
t.Run("unix timestamps", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"start_date", "end_date", "due_date"},
|
||||
"filter_value": []string{"1544500000", "1513164001", "1543500000"},
|
||||
"filter_comparator": []string{"greater", "less", "greater"},
|
||||
},
|
||||
urlParams,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.Contains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.Contains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.Contains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #13`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
})
|
||||
t.Run("invalid date", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"due_date"},
|
||||
"filter_value": []string{"1540000000"},
|
||||
"filter_value": []string{"invalid"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
},
|
||||
nil,
|
||||
|
@ -341,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"}},{"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,"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-4","index":4,"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":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"}}]`)
|
||||
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"}},{"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,"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-4","index":4,"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":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"}}]`)
|
||||
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
|
||||
|
@ -451,7 +476,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
_, err := testHandler.testReadAllWithUser(
|
||||
url.Values{
|
||||
"filter_by": []string{"due_date"},
|
||||
"filter_value": []string{"1540000000"},
|
||||
"filter_value": []string{"invalid"},
|
||||
"filter_comparator": []string{"greater"},
|
||||
},
|
||||
nil,
|
||||
|
|
|
@ -63,6 +63,7 @@ func TestTaskComments(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
// Only the own comments can be updated
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "14", "commentid": "2"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
|
@ -74,14 +75,14 @@ func TestTaskComments(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{"task": "16", "commentid": "4"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "16", "commentid": "4"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "17", "commentid": "5"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "17", "commentid": "5"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||
|
@ -90,14 +91,14 @@ func TestTaskComments(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{"task": "19", "commentid": "7"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "19", "commentid": "7"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "20", "commentid": "8"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "20", "commentid": "8"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
|
@ -106,14 +107,14 @@ func TestTaskComments(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{"task": "22", "commentid": "10"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "22", "commentid": "10"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "23", "commentid": "11"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "23", "commentid": "11"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
|
@ -122,14 +123,14 @@ func TestTaskComments(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{"task": "25", "commentid": "13"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "25", "commentid": "13"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "26", "commentid": "14"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "26", "commentid": "14"}, `{"comment":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -145,6 +146,7 @@ func TestTaskComments(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
// Only the own comments can be deleted
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "14", "commentid": "2"})
|
||||
assert.Error(t, err)
|
||||
|
@ -156,14 +158,14 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "16", "commentid": "4"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "16", "commentid": "4"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "17", "commentid": "5"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "17", "commentid": "5"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||
|
@ -172,14 +174,14 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "19", "commentid": "7"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "19", "commentid": "7"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "20", "commentid": "8"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "20", "commentid": "8"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
|
@ -188,14 +190,14 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "22", "commentid": "10"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "22", "commentid": "10"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "23", "commentid": "11"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "23", "commentid": "11"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
|
@ -204,14 +206,14 @@ func TestTaskComments(t *testing.T) {
|
|||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "25", "commentid": "13"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "25", "commentid": "13"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "26", "commentid": "14"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "26", "commentid": "14"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -63,23 +63,26 @@ func InitLogger() {
|
|||
}
|
||||
}
|
||||
|
||||
// We define our two backends
|
||||
if config.LogStandard.GetString() != "off" {
|
||||
// The backend is the part which actually handles logging the log entries somewhere.
|
||||
cf := config.LogStandard.GetString()
|
||||
var backend logging.Backend
|
||||
backend = &NoopBackend{}
|
||||
if cf != "off" && cf != "false" {
|
||||
stdWriter := GetLogWriter("standard")
|
||||
|
||||
level, err := logging.LogLevel(strings.ToUpper(config.LogLevel.GetString()))
|
||||
if err != nil {
|
||||
Fatalf("Error setting database log level: %s", err.Error())
|
||||
}
|
||||
|
||||
logBackend := logging.NewLogBackend(stdWriter, "", 0)
|
||||
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(Fmt+"\n"))
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(level, logModule)
|
||||
|
||||
logInstance.SetBackend(backendLeveled)
|
||||
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(Fmt+"\n"))
|
||||
}
|
||||
|
||||
level, err := logging.LogLevel(strings.ToUpper(config.LogLevel.GetString()))
|
||||
if err != nil {
|
||||
Fatalf("Error setting database log level: %s", err.Error())
|
||||
}
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(level, logModule)
|
||||
|
||||
logInstance.SetBackend(backendLeveled)
|
||||
}
|
||||
|
||||
// GetLogWriter returns the writer to where the normal log goes, depending on the config
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// 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 log
|
||||
|
||||
import (
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
// NoopBackend doesn't log anything. Used in cases where we want to disable logging completely.
|
||||
type NoopBackend struct{}
|
||||
|
||||
func (n *NoopBackend) Log(level logging.Level, i int, record *logging.Record) error {
|
||||
return nil
|
||||
}
|
|
@ -45,8 +45,13 @@ func NewWatermillLogger() *WatermillLogger {
|
|||
logger: logging.MustGetLogger(watermillLogModule),
|
||||
}
|
||||
|
||||
logBackend := logging.NewLogBackend(GetLogWriter("events"), "", 0)
|
||||
backend := logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(watermillFmt+"\n"))
|
||||
cf := config.LogEvents.GetString()
|
||||
var backend logging.Backend
|
||||
backend = &NoopBackend{}
|
||||
if cf != "off" && cf != "false" {
|
||||
logBackend := logging.NewLogBackend(GetLogWriter("events"), "", 0)
|
||||
backend = logging.NewBackendFormatter(logBackend, logging.MustStringFormatter(watermillFmt+"\n"))
|
||||
}
|
||||
|
||||
backendLeveled := logging.AddModuleLevel(backend)
|
||||
backendLeveled.SetLevel(level, watermillLogModule)
|
||||
|
|
|
@ -17,31 +17,68 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"gopkg.in/gomail.v2"
|
||||
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
// Queue is the mail queue
|
||||
var Queue chan *gomail.Message
|
||||
var Queue chan *mail.Msg
|
||||
|
||||
func getDialer() *gomail.Dialer {
|
||||
d := gomail.NewDialer(config.MailerHost.GetString(), config.MailerPort.GetInt(), config.MailerUsername.GetString(), config.MailerPassword.GetString())
|
||||
// #nosec
|
||||
d.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool(),
|
||||
ServerName: config.MailerHost.GetString(),
|
||||
func getClient() (*mail.Client, error) {
|
||||
|
||||
var authType mail.SMTPAuthType
|
||||
switch config.MailerAuthType.GetString() {
|
||||
case "plain":
|
||||
authType = mail.SMTPAuthPlain
|
||||
case "login":
|
||||
authType = mail.SMTPAuthLogin
|
||||
case "cram-md5":
|
||||
authType = mail.SMTPAuthCramMD5
|
||||
}
|
||||
d.SSL = config.MailerForceSSL.GetBool()
|
||||
return d
|
||||
|
||||
tlsPolicy := mail.TLSOpportunistic
|
||||
if config.MailerForceSSL.GetBool() {
|
||||
tlsPolicy = mail.TLSMandatory
|
||||
}
|
||||
|
||||
opts := []mail.Option{
|
||||
mail.WithPort(config.MailerPort.GetInt()),
|
||||
mail.WithTLSPolicy(tlsPolicy),
|
||||
//#nosec G402
|
||||
mail.WithTLSConfig(&tls.Config{
|
||||
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.MailerUsername.GetString() != "" && config.MailerPassword.GetString() != "" {
|
||||
opts = append(opts, mail.WithSMTPAuth(authType))
|
||||
}
|
||||
|
||||
if config.MailerUsername.GetString() != "" {
|
||||
opts = append(opts, mail.WithUsername(config.MailerUsername.GetString()))
|
||||
}
|
||||
|
||||
if config.MailerPassword.GetString() != "" {
|
||||
opts = append(opts, mail.WithPassword(config.MailerPassword.GetString()))
|
||||
}
|
||||
|
||||
return mail.NewClient(
|
||||
config.MailerHost.GetString(),
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
// StartMailDaemon starts the mail daemon
|
||||
func StartMailDaemon() {
|
||||
Queue = make(chan *gomail.Message, config.MailerQueuelength.GetInt())
|
||||
Queue = make(chan *mail.Msg, config.MailerQueuelength.GetInt())
|
||||
|
||||
if !config.MailerEnabled.GetBool() {
|
||||
return
|
||||
|
@ -52,10 +89,12 @@ func StartMailDaemon() {
|
|||
return
|
||||
}
|
||||
|
||||
c, err := getClient()
|
||||
if err != nil {
|
||||
log.Errorf("Could not create mail client: %v", err)
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
d := getDialer()
|
||||
|
||||
var s gomail.SendCloser
|
||||
var err error
|
||||
open := false
|
||||
for {
|
||||
|
@ -65,14 +104,16 @@ func StartMailDaemon() {
|
|||
return
|
||||
}
|
||||
if !open {
|
||||
if s, err = d.Dial(); err != nil {
|
||||
log.Error("Error during connect to smtp server: %s", err)
|
||||
err = c.DialWithContext(context.Background())
|
||||
if err != nil {
|
||||
log.Errorf("Error during connect to smtp server: %s", err)
|
||||
break
|
||||
}
|
||||
open = true
|
||||
}
|
||||
if err := gomail.Send(s, m); err != nil {
|
||||
log.Error("Error when sending mail: %s", err)
|
||||
err = c.Send(m)
|
||||
if err != nil {
|
||||
log.Errorf("Error when sending mail: %s", err)
|
||||
break
|
||||
}
|
||||
// Close the connection to the SMTP server if no email was sent in
|
||||
|
@ -80,18 +121,14 @@ func StartMailDaemon() {
|
|||
case <-time.After(config.MailerQueueTimeout.GetDuration() * time.Second):
|
||||
if open {
|
||||
open = false
|
||||
if err := s.Close(); err != nil {
|
||||
log.Error("Error closing the mail server connection: %s\n", err)
|
||||
err = c.Close()
|
||||
if err != nil {
|
||||
log.Errorf("Error closing the mail server connection: %s\n", err)
|
||||
break
|
||||
}
|
||||
log.Infof("Closed connection to mailserver")
|
||||
log.Info("Closed connection to mail server")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// StopMailDaemon closes the mail queue channel, aka stops the daemon
|
||||
func StopMailDaemon() {
|
||||
close(Queue)
|
||||
}
|
||||
|
|
|
@ -17,9 +17,14 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"gopkg.in/gomail.v2"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
// Opts holds infos for a mail
|
||||
|
@ -32,6 +37,8 @@ type Opts struct {
|
|||
ContentType ContentType
|
||||
Boundary string
|
||||
Headers []*header
|
||||
Embeds map[string]io.Reader
|
||||
EmbedFS map[string]*embed.FS
|
||||
}
|
||||
|
||||
// ContentType represents mail content types
|
||||
|
@ -45,11 +52,11 @@ const (
|
|||
)
|
||||
|
||||
type header struct {
|
||||
Field string
|
||||
Field mail.Header
|
||||
Content string
|
||||
}
|
||||
|
||||
// SendTestMail sends a test mail to a receipient.
|
||||
// SendTestMail sends a test mail to a recipient.
|
||||
// It works without a queue.
|
||||
func SendTestMail(opts *Opts) error {
|
||||
if config.MailerHost.GetString() == "" {
|
||||
|
@ -57,39 +64,51 @@ func SendTestMail(opts *Opts) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
d := getDialer()
|
||||
s, err := d.Dial()
|
||||
c, err := getClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
m := sendMail(opts)
|
||||
m := getMessage(opts)
|
||||
|
||||
return gomail.Send(s, m)
|
||||
return c.DialAndSend(m)
|
||||
}
|
||||
|
||||
func sendMail(opts *Opts) *gomail.Message {
|
||||
m := gomail.NewMessage()
|
||||
func getMessage(opts *Opts) *mail.Msg {
|
||||
m := mail.NewMsg()
|
||||
m.SetUserAgent("Vikunja " + version.Version)
|
||||
if opts.From == "" {
|
||||
opts.From = "Vikunja <" + config.MailerFromEmail.GetString() + ">"
|
||||
}
|
||||
m.SetHeader("From", opts.From)
|
||||
m.SetHeader("To", opts.To)
|
||||
m.SetHeader("Subject", opts.Subject)
|
||||
_ = m.From(opts.From)
|
||||
_ = m.To(opts.To)
|
||||
m.Subject(opts.Subject)
|
||||
|
||||
for _, h := range opts.Headers {
|
||||
m.SetHeader(h.Field, h.Content)
|
||||
}
|
||||
|
||||
for name, content := range opts.Embeds {
|
||||
m.EmbedReader(name, content)
|
||||
}
|
||||
|
||||
for name, fs := range opts.EmbedFS {
|
||||
err := m.EmbedFromEmbedFS(name, fs)
|
||||
if err != nil {
|
||||
log.Errorf("Error embedding %s via embed.FS into mail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
switch opts.ContentType {
|
||||
case ContentTypePlain:
|
||||
m.SetBody("text/plain", opts.Message)
|
||||
m.SetBodyString("text/plain", opts.Message)
|
||||
case ContentTypeHTML:
|
||||
m.SetBody("text/html", opts.Message)
|
||||
m.SetBodyString("text/html", opts.Message)
|
||||
case ContentTypeMultipart:
|
||||
m.SetBody("text/plain", opts.Message)
|
||||
m.AddAlternative("text/html", opts.HTMLMessage)
|
||||
m.SetBodyString("text/plain", opts.Message)
|
||||
m.AddAlternativeString("text/html", opts.HTMLMessage)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
|
@ -100,6 +119,6 @@ func SendMail(opts *Opts) {
|
|||
return
|
||||
}
|
||||
|
||||
m := sendMail(opts)
|
||||
m := getMessage(opts)
|
||||
Queue <- m
|
||||
}
|
||||
|
|
|
@ -683,17 +683,17 @@ create unique index UQE_users_namespace_id
|
|||
|
||||
sess := tx.NewSession()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return fmt.Errorf("unable to open session: %s", err)
|
||||
return fmt.Errorf("unable to open session: %w", err)
|
||||
}
|
||||
for _, s := range sql {
|
||||
_, err := sess.Exec(s)
|
||||
if err != nil {
|
||||
_ = sess.Rollback()
|
||||
return fmt.Errorf("error executing update data for table %s, column %s: %s", table, column, err)
|
||||
return fmt.Errorf("error executing update data for table %s, column %s: %w", table, column, err)
|
||||
}
|
||||
}
|
||||
if err := sess.Commit(); err != nil {
|
||||
return fmt.Errorf("error committing data change: %s", err)
|
||||
return fmt.Errorf("error committing data change: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type user20211212151642 struct {
|
||||
Language string `xorm:"varchar(50) null" json:"-"`
|
||||
}
|
||||
|
||||
func (user20211212151642) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20211212151642",
|
||||
Description: "Add user language field",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(user20211212151642{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
// 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 (
|
||||
"errors"
|
||||
"image"
|
||||
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"github.com/bbrks/go-blurhash"
|
||||
"golang.org/x/image/draw"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type lists20211212210054 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"list"`
|
||||
BackgroundFileID int64 `xorm:"null" json:"-"`
|
||||
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
|
||||
}
|
||||
|
||||
func (lists20211212210054) TableName() string {
|
||||
return "lists"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20211212210054",
|
||||
Description: "Add blurHash to list backgrounds.",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(lists20211212210054{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lists := []*lists20211212210054{}
|
||||
err = tx.Where("background_file_id is not null AND background_file_id != ?", 0).Find(&lists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Creating BlurHash for %d list backgrounds, this might take a while...", len(lists))
|
||||
|
||||
for _, l := range lists {
|
||||
bgFile := &files.File{
|
||||
ID: l.BackgroundFileID,
|
||||
}
|
||||
if err := bgFile.LoadFileByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
src, _, err := image.Decode(bgFile.File)
|
||||
if err != nil && !errors.Is(err, image.ErrFormat) {
|
||||
return err
|
||||
}
|
||||
if err != nil && errors.Is(err, image.ErrFormat) {
|
||||
log.Warningf("Could not generate a blur hash of list %d's background image: %s", l.ID, err)
|
||||
}
|
||||
|
||||
dst := image.NewRGBA(image.Rect(0, 0, 32, 32))
|
||||
draw.NearestNeighbor.Scale(dst, dst.Rect, src, src.Bounds(), draw.Over, nil)
|
||||
|
||||
hash, err := blurhash.Encode(4, 3, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.BackgroundBlurHash = hash
|
||||
_, err = tx.Where("id = ?", l.ID).
|
||||
Cols("background_blur_hash").
|
||||
Update(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("Created BlurHash for list %d", l.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// 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 (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type users20220112211537 struct {
|
||||
Timezone string `xorm:"varchar(255) null" json:"-"`
|
||||
}
|
||||
|
||||
func (users20220112211537) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20220112211537",
|
||||
Description: "Add time zone setting for users",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(users20220112211537{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Update(&users20220112211537{Timezone: config.GetTimeZone().String()})
|
||||
return err
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type users20220616145228 struct {
|
||||
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
|
||||
}
|
||||
|
||||
func (users20220616145228) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20220616145228",
|
||||
Description: "Add overdue task summary time field to users",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20220616145228{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
// 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 (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20220815200851",
|
||||
Description: "Migrate saved assignee filter to usernames instead of IDs",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
filters := []map[string]interface{}{} // not using the type here so that the migration does not depend on it
|
||||
err := tx.Select("*").
|
||||
Table("saved_filters").
|
||||
Find(&filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, f := range filters {
|
||||
filter := map[string]interface{}{}
|
||||
filterJSON, is := f["filters"].(string)
|
||||
if !is {
|
||||
continue
|
||||
}
|
||||
err = json.Unmarshal([]byte(filterJSON), &filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filterBy := filter["filter_by"].([]interface{})
|
||||
filterValue := filter["filter_value"].([]interface{})
|
||||
for p, fb := range filterBy {
|
||||
if fb == "assignees" || fb == "user_id" {
|
||||
userIDs := []int64{}
|
||||
for _, sid := range strings.Split(filterValue[p].(string), ",") {
|
||||
id, err := strconv.ParseInt(sid, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userIDs = append(userIDs, id)
|
||||
}
|
||||
|
||||
usernames := []string{}
|
||||
err := tx.Select("username").
|
||||
Table("users").
|
||||
In("id", userIDs).
|
||||
Find(&usernames)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userfilter := ""
|
||||
for i, username := range usernames {
|
||||
if i > 0 {
|
||||
userfilter += ","
|
||||
}
|
||||
userfilter += username
|
||||
}
|
||||
filterValue[p] = userfilter
|
||||
}
|
||||
}
|
||||
|
||||
filter["filter_value"] = filterValue
|
||||
filtersJSON, err := json.Marshal(filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f["filters"] = string(filtersJSON)
|
||||
|
||||
_, err = tx.Where("id = ?", f["id"]).
|
||||
Cols("filters").
|
||||
NoAutoCondition().
|
||||
Table("saved_filters").
|
||||
Update(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type 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
|
||||
},
|
||||
})
|
||||
}
|
|
@ -237,7 +237,7 @@ type ErrListIsArchived struct {
|
|||
ListID int64
|
||||
}
|
||||
|
||||
// IsErrListIsArchived checks if an error is a .
|
||||
// IsErrListIsArchived checks if an error is a list is archived error.
|
||||
func IsErrListIsArchived(err error) bool {
|
||||
_, ok := err.(ErrListIsArchived)
|
||||
return ok
|
||||
|
@ -255,6 +255,62 @@ func (err ErrListIsArchived) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeListIsArchived, Message: "This list is archived. Editing or creating new tasks is not possible."}
|
||||
}
|
||||
|
||||
// ErrListCannotBelongToAPseudoNamespace represents an error where a list cannot belong to a pseudo namespace
|
||||
type ErrListCannotBelongToAPseudoNamespace struct {
|
||||
ListID int64
|
||||
NamespaceID int64
|
||||
}
|
||||
|
||||
// IsErrListCannotBelongToAPseudoNamespace checks if an error is a list is archived error.
|
||||
func IsErrListCannotBelongToAPseudoNamespace(err error) bool {
|
||||
_, ok := err.(*ErrListCannotBelongToAPseudoNamespace)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrListCannotBelongToAPseudoNamespace) Error() string {
|
||||
return fmt.Sprintf("List cannot belong to a pseudo namespace [ListID: %d, NamespaceID: %d]", err.ListID, err.NamespaceID)
|
||||
}
|
||||
|
||||
// ErrCodeListCannotBelongToAPseudoNamespace holds the unique world-error code of this error
|
||||
const ErrCodeListCannotBelongToAPseudoNamespace = 3009
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrListCannotBelongToAPseudoNamespace) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeListCannotBelongToAPseudoNamespace,
|
||||
Message: "This list cannot belong a dynamically generated namespace.",
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// ================
|
||||
|
@ -763,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
|
||||
// =================
|
||||
|
|
|
@ -38,7 +38,7 @@ import (
|
|||
)
|
||||
|
||||
func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
||||
exportDir := config.ServiceRootpath.GetString() + "/files/user-export-tmp/"
|
||||
exportDir := config.FilesBasePath.GetString() + "/user-export-tmp/"
|
||||
err = os.MkdirAll(exportDir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -49,7 +49,7 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
|||
// Open zip
|
||||
dumpFile, err := os.Create(tmpFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening dump file: %s", err)
|
||||
return fmt.Errorf("error opening dump file: %w", err)
|
||||
}
|
||||
defer dumpFile.Close()
|
||||
|
||||
|
@ -123,13 +123,15 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
|||
|
||||
func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
|
||||
namspaces, _, _, err := (&Namespace{}).ReadAll(s, u, "", -1, 0)
|
||||
namspaces, _, _, err := (&Namespace{IsArchived: true}).ReadAll(s, u, "", -1, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
namespaceIDs := []int64{}
|
||||
namespaces := []*NamespaceWithListsAndTasks{}
|
||||
listMap := make(map[int64]*ListWithTasksAndBuckets)
|
||||
listIDs := []int64{}
|
||||
for _, n := range namspaces.([]*NamespaceWithLists) {
|
||||
if n.ID < 1 {
|
||||
// Don't include filters
|
||||
|
@ -142,11 +144,14 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
|
|||
}
|
||||
|
||||
for _, l := range n.Lists {
|
||||
nn.Lists = append(nn.Lists, &ListWithTasksAndBuckets{
|
||||
ll := &ListWithTasksAndBuckets{
|
||||
List: *l,
|
||||
BackgroundFileID: l.BackgroundFileID,
|
||||
Tasks: []*TaskWithComments{},
|
||||
})
|
||||
}
|
||||
nn.Lists = append(nn.Lists, ll)
|
||||
listMap[l.ID] = ll
|
||||
listIDs = append(listIDs, l.ID)
|
||||
}
|
||||
|
||||
namespaceIDs = append(namespaceIDs, n.ID)
|
||||
|
@ -171,20 +176,15 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
|
|||
return err
|
||||
}
|
||||
|
||||
listMap := make(map[int64]*ListWithTasksAndBuckets)
|
||||
listIDs := []int64{}
|
||||
for _, n := range namespaces {
|
||||
for _, l := range n.Lists {
|
||||
listMap[l.ID] = l
|
||||
listIDs = append(listIDs, l.ID)
|
||||
}
|
||||
}
|
||||
|
||||
taskMap := make(map[int64]*TaskWithComments, len(tasks))
|
||||
for _, t := range tasks {
|
||||
taskMap[t.ID] = &TaskWithComments{
|
||||
Task: *t,
|
||||
}
|
||||
if _, exists := listMap[t.ListID]; !exists {
|
||||
log.Debugf("[User Data Export] List %d does not exist for task %d, omitting", t.ListID, t.ID)
|
||||
continue
|
||||
}
|
||||
listMap[t.ListID].Tasks = append(listMap[t.ListID].Tasks, taskMap[t.ID])
|
||||
}
|
||||
|
||||
|
@ -198,6 +198,10 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
|
|||
}
|
||||
|
||||
for _, c := range comments {
|
||||
if _, exists := taskMap[c.TaskID]; !exists {
|
||||
log.Debugf("[User Data Export] Task %d does not exist for comment %d, omitting", c.TaskID, c.ID)
|
||||
continue
|
||||
}
|
||||
taskMap[c.TaskID].Comments = append(taskMap[c.TaskID].Comments, c)
|
||||
}
|
||||
|
||||
|
@ -208,6 +212,10 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err
|
|||
}
|
||||
|
||||
for _, b := range buckets {
|
||||
if _, exists := listMap[b.ListID]; !exists {
|
||||
log.Debugf("[User Data Export] List %d does not exist for bucket %d, omitting", b.ListID, b.ID)
|
||||
continue
|
||||
}
|
||||
listMap[b.ListID].Buckets = append(listMap[b.ListID].Buckets, b)
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -54,6 +54,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -104,6 +105,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -168,6 +170,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -229,6 +232,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -59,6 +59,8 @@ type List struct {
|
|||
BackgroundFileID int64 `xorm:"null" json:"-"`
|
||||
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background
|
||||
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
|
||||
// Contains a very small version of the list background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
|
||||
BackgroundBlurHash string `xorm:"varchar(50) null" json:"background_blur_hash"`
|
||||
|
||||
// True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.
|
||||
IsFavorite bool `xorm:"-" json:"is_favorite"`
|
||||
|
@ -474,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 {
|
||||
|
@ -540,12 +552,14 @@ func (l *List) CheckIsArchived(s *xorm.Session) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
||||
func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
|
||||
func checkListBeforeUpdateOrDelete(s *xorm.Session, list *List) error {
|
||||
if list.NamespaceID < 0 {
|
||||
return &ErrListCannotBelongToAPseudoNamespace{ListID: list.ID, NamespaceID: list.NamespaceID}
|
||||
}
|
||||
|
||||
// Check if the namespace exists
|
||||
if list.NamespaceID != 0 && list.NamespaceID != FavoritesPseudoNamespace.ID {
|
||||
_, err = GetNamespaceByID(s, list.NamespaceID)
|
||||
if list.NamespaceID > 0 {
|
||||
_, err := GetNamespaceByID(s, list.NamespaceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -565,65 +579,123 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
|
|||
}
|
||||
}
|
||||
|
||||
if list.ID == 0 {
|
||||
_, err = s.Insert(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
list.Position = calculateDefaultPosition(list.ID, list.Position)
|
||||
_, err = s.Where("id = ?", list.ID).Update(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list.IsFavorite {
|
||||
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||
colsToUpdate := []string{
|
||||
"title",
|
||||
"is_archived",
|
||||
"identifier",
|
||||
"hex_color",
|
||||
"background_file_id",
|
||||
"position",
|
||||
}
|
||||
if list.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
func CreateList(s *xorm.Session, list *List, auth web.Auth) (err error) {
|
||||
err = list.CheckIsArchived(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if list.IsFavorite && !wasFavorite {
|
||||
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
doer, err := user.GetFromAuth(auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !list.IsFavorite && wasFavorite {
|
||||
if err := removeFromFavorite(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
list.OwnerID = doer.ID
|
||||
list.Owner = doer
|
||||
list.ID = 0 // Otherwise only the first time a new list would be created
|
||||
|
||||
_, err = s.
|
||||
ID(list.ID).
|
||||
Cols(colsToUpdate...).
|
||||
Update(list)
|
||||
if err != nil {
|
||||
err = checkListBeforeUpdateOrDelete(s, list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.Insert(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
list.Position = calculateDefaultPosition(list.ID, list.Position)
|
||||
_, err = s.Where("id = ?", list.ID).Update(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list.IsFavorite {
|
||||
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new first bucket for this list
|
||||
b := &Bucket{
|
||||
ListID: list.ID,
|
||||
Title: "Backlog",
|
||||
}
|
||||
err = b.Create(s, auth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListCreatedEvent{
|
||||
List: list,
|
||||
Doer: doer,
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateList(s *xorm.Session, list *List, auth web.Auth, updateListBackground bool) (err error) {
|
||||
err = checkListBeforeUpdateOrDelete(s, list)
|
||||
if err != nil {
|
||||
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",
|
||||
"is_archived",
|
||||
"identifier",
|
||||
"hex_color",
|
||||
"namespace_id",
|
||||
"position",
|
||||
}
|
||||
if list.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
|
||||
if updateListBackground {
|
||||
colsToUpdate = append(colsToUpdate, "background_file_id", "background_blur_hash")
|
||||
}
|
||||
|
||||
wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if list.IsFavorite && !wasFavorite {
|
||||
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !list.IsFavorite && wasFavorite {
|
||||
if err := removeFromFavorite(s, list.ID, auth, FavoriteKindList); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
ID(list.ID).
|
||||
Cols(colsToUpdate...).
|
||||
Update(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = events.Dispatch(&ListUpdatedEvent{
|
||||
List: list,
|
||||
Doer: auth,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l, err := GetListSimpleByID(s, list.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -668,15 +740,7 @@ func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
err = CreateOrUpdateList(s, l, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListUpdatedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
})
|
||||
return UpdateList(s, l, a, false)
|
||||
}
|
||||
|
||||
func updateListLastUpdated(s *xorm.Session, list *List) error {
|
||||
|
@ -709,39 +773,12 @@ func updateListByTaskID(s *xorm.Session, taskID int64) (err error) {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces/{namespaceID}/lists [put]
|
||||
func (l *List) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
err = l.CheckIsArchived(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doer, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.OwnerID = doer.ID
|
||||
l.Owner = doer
|
||||
l.ID = 0 // Otherwise only the first time a new list would be created
|
||||
|
||||
err = CreateOrUpdateList(s, l, a)
|
||||
err = CreateList(s, l, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new first bucket for this list
|
||||
b := &Bucket{
|
||||
ListID: l.ID,
|
||||
Title: "Backlog",
|
||||
}
|
||||
err = b.Create(s, a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListCreatedEvent{
|
||||
List: l,
|
||||
Doer: doer,
|
||||
})
|
||||
return l.ReadOne(s, a)
|
||||
}
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
|
@ -785,14 +822,15 @@ func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// SetListBackground sets a background file as list background in the db
|
||||
func SetListBackground(s *xorm.Session, listID int64, background *files.File) (err error) {
|
||||
func SetListBackground(s *xorm.Session, listID int64, background *files.File, blurHash string) (err error) {
|
||||
l := &List{
|
||||
ID: listID,
|
||||
BackgroundFileID: background.ID,
|
||||
ID: listID,
|
||||
BackgroundFileID: background.ID,
|
||||
BackgroundBlurHash: blurHash,
|
||||
}
|
||||
_, err = s.
|
||||
Where("id = ?", l.ID).
|
||||
Cols("background_file_id").
|
||||
Cols("background_file_id", "background_blur_hash").
|
||||
Update(l)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
@ -75,7 +76,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
ld.List.Identifier = "" // Reset the identifier to trigger regenerating a new one
|
||||
// Set the owner to the current user
|
||||
ld.List.OwnerID = doer.GetID()
|
||||
if err := CreateOrUpdateList(s, ld.List, doer); err != nil {
|
||||
if err := CreateList(s, ld.List, doer); err != nil {
|
||||
// If there is no available unique list identifier, just reset it.
|
||||
if IsErrListIdentifierIsNotUnique(err) {
|
||||
ld.List.Identifier = ""
|
||||
|
@ -144,7 +145,7 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
if err := SetListBackground(s, ld.List.ID, file); err != nil {
|
||||
if err := SetListBackground(s, ld.List.ID, file, ld.List.BackgroundBlurHash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -216,7 +217,7 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap
|
|||
// It is used to map old task items to new ones.
|
||||
taskMap := make(map[int64]int64)
|
||||
// Create + update all tasks (includes reminders)
|
||||
oldTaskIDs := make([]int64, len(tasks))
|
||||
oldTaskIDs := make([]int64, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
oldID := t.ID
|
||||
t.ID = 0
|
||||
|
|
|
@ -116,6 +116,25 @@ func (l *List) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err error
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// Get the list
|
||||
ol, err := GetListSimpleByID(s, l.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if we're moving the list into a different namespace.
|
||||
// If that is the case, we need to verify permissions to do so.
|
||||
if l.NamespaceID != 0 && l.NamespaceID != ol.NamespaceID {
|
||||
newNamespace := &Namespace{ID: l.NamespaceID}
|
||||
can, err := newNamespace.CanWrite(s, a)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !can {
|
||||
return false, ErrGenericForbidden{}
|
||||
}
|
||||
}
|
||||
|
||||
fid := getSavedFilterIDFromListID(l.ID)
|
||||
if fid > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, fid)
|
||||
|
@ -183,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
|
||||
|
@ -223,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
|
||||
|
@ -266,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
|
||||
}
|
||||
|
|
|
@ -140,8 +140,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)
|
||||
|
@ -163,6 +164,84 @@ func TestList_CreateOrUpdate(t *testing.T) {
|
|||
assert.True(t, IsErrListIdentifierIsNotUnique(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("change namespace", func(t *testing.T) {
|
||||
t.Run("own", func(t *testing.T) {
|
||||
usr := &user.User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Email: "user6@example.com",
|
||||
}
|
||||
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 6,
|
||||
Title: "Test6",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 7, // from 6
|
||||
}
|
||||
can, err := list.CanUpdate(s, usr)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, can)
|
||||
err = list.Update(s, usr)
|
||||
assert.NoError(t, err)
|
||||
err = s.Commit()
|
||||
assert.NoError(t, err)
|
||||
db.AssertExists(t, "lists", map[string]interface{}{
|
||||
"id": list.ID,
|
||||
"title": list.Title,
|
||||
"description": list.Description,
|
||||
"namespace_id": list.NamespaceID,
|
||||
}, false)
|
||||
})
|
||||
// FIXME: The check for whether the namespace is archived is missing in namespace.CanWrite
|
||||
// t.Run("archived own", func(t *testing.T) {
|
||||
// db.LoadAndAssertFixtures(t)
|
||||
// s := db.NewSession()
|
||||
// list := List{
|
||||
// ID: 1,
|
||||
// Title: "Test1",
|
||||
// Description: "Lorem Ipsum",
|
||||
// NamespaceID: 16, // from 1
|
||||
// }
|
||||
// can, err := list.CanUpdate(s, usr)
|
||||
// assert.NoError(t, err)
|
||||
// assert.False(t, can) // namespace is archived and thus not writeable
|
||||
// _ = s.Close()
|
||||
// })
|
||||
t.Run("others", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 1,
|
||||
Title: "Test1",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: 2, // from 1
|
||||
}
|
||||
can, _ := list.CanUpdate(s, usr)
|
||||
assert.False(t, can) // namespace is not writeable by us
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("pseudo namespace", func(t *testing.T) {
|
||||
usr := &user.User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Email: "user6@example.com",
|
||||
}
|
||||
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
list := List{
|
||||
ID: 6,
|
||||
Title: "Test6",
|
||||
Description: "Lorem Ipsum",
|
||||
NamespaceID: -1,
|
||||
}
|
||||
err := list.Update(s, usr)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrListCannotBelongToAPseudoNamespace(err))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -151,6 +151,7 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -164,6 +165,7 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -247,6 +247,11 @@ func (s *SendTaskAssignedNotification) Handle(msg *message.Message) (err error)
|
|||
|
||||
log.Debugf("Sending task assigned notifications to %d subscribers for task %d", len(subscribers), event.Task.ID)
|
||||
|
||||
task, err := GetTaskByIDSimple(sess, event.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, subscriber := range subscribers {
|
||||
if subscriber.UserID == event.Doer.ID {
|
||||
continue
|
||||
|
@ -254,7 +259,7 @@ func (s *SendTaskAssignedNotification) Handle(msg *message.Message) (err error)
|
|||
|
||||
n := &TaskAssignedNotification{
|
||||
Doer: event.Doer,
|
||||
Task: event.Task,
|
||||
Task: &task,
|
||||
Assignee: event.Assignee,
|
||||
}
|
||||
err = notifications.Notify(subscriber.User, n)
|
||||
|
|
|
@ -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)
|
||||
|
@ -603,20 +611,17 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /namespaces [put]
|
||||
func (n *Namespace) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
// Check if we have at least a name
|
||||
// Check if we have at least a title
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()}
|
||||
}
|
||||
n.ID = 0 // This would otherwise prevent the creation of new lists after one was created
|
||||
|
||||
// Check if the User exists
|
||||
n.Owner, err = user.GetUserByID(s, a.GetID())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n.OwnerID = n.Owner.ID
|
||||
|
||||
// Insert
|
||||
if _, err = s.Insert(n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -163,6 +164,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ package models
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -112,7 +113,7 @@ type TaskAssignedNotification struct {
|
|||
func (n *TaskAssignedNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject(n.Task.Title+"("+n.Task.GetFullIdentifier()+")"+" has been assigned to "+n.Assignee.GetName()).
|
||||
Line(n.Doer.GetName()+" has assigned this task to "+n.Assignee.GetName()).
|
||||
Line(n.Doer.GetName()+" has assigned this task to "+n.Assignee.GetName()+".").
|
||||
Action("View Task", n.Task.GetFrontendURL())
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -221,6 +221,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,
|
||||
|
|
|
@ -131,7 +131,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskOptions, err
|
|||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
|
||||
// @Param filter_by query string false "The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match."
|
||||
// @Param filter_value query string false "The value to filter for."
|
||||
// @Param filter_value query string false "The value to filter for. You can use [grafana](https://grafana.com/docs/grafana/latest/dashboards/time-range-controls)- or [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/common-options.html#date-math)-style relative dates for all date fields like `due_date`, `start_date`, `end_date`, etc."
|
||||
// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals`"
|
||||
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
|
||||
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
|
||||
|
@ -149,6 +149,11 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
sf.Filters.SortByArr = tf.SortByArr
|
||||
sf.Filters.SortBy = tf.SortBy
|
||||
sf.Filters.OrderByArr = tf.OrderByArr
|
||||
sf.Filters.OrderBy = tf.OrderBy
|
||||
|
||||
return sf.getTaskCollection().ReadAll(s, a, search, page, perPage)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/iancoleman/strcase"
|
||||
"github.com/vectordotdev/go-datemath"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
|
@ -159,8 +160,14 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
|
|||
value, err = strconv.ParseBool(rawValue)
|
||||
case reflect.Struct:
|
||||
if field.Type == schemas.TimeType {
|
||||
value, err = time.Parse(time.RFC3339, rawValue)
|
||||
value = value.(time.Time).In(config.GetTimeZone())
|
||||
var t datemath.Expression
|
||||
t, err = datemath.Parse(rawValue)
|
||||
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())
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
// If this is a slice of pointers we're dealing with some property which is a relation
|
||||
|
@ -207,6 +214,12 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
|
|||
return
|
||||
}
|
||||
|
||||
if realFieldName == "Assignees" {
|
||||
vals := strings.Split(value, ",")
|
||||
valueSlice := append([]string{}, vals...)
|
||||
return valueSlice, nil
|
||||
}
|
||||
|
||||
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName)
|
||||
if !ok {
|
||||
return nil, ErrInvalidTaskField{TaskField: fieldName}
|
||||
|
|
|
@ -37,6 +37,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -47,6 +48,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -57,6 +59,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -931,10 +934,10 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter assignees",
|
||||
name: "filter assignees by username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"1"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
|
@ -944,12 +947,80 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter assignees in",
|
||||
name: "filter assignees by username with users field name",
|
||||
fields: fields{
|
||||
FilterBy: []string{"users"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "filter assignees by username with user_id field name",
|
||||
fields: fields{
|
||||
FilterBy: []string{"user_id"},
|
||||
FilterValue: []string{"user1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "filter assignees by multiple username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees", "assignees"},
|
||||
FilterValue: []string{"user1", "user2"},
|
||||
FilterComparator: []string{"equals", "equals"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task30,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter assignees by numbers",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"1"},
|
||||
FilterComparator: []string{"equals"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter assignees by name with like",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user"},
|
||||
FilterComparator: []string{"like"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "filter assignees in by id",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"1,2"},
|
||||
FilterComparator: []string{"in"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "filter assignees in by username",
|
||||
fields: fields{
|
||||
FilterBy: []string{"assignees"},
|
||||
FilterValue: []string{"user1,user2"},
|
||||
FilterComparator: []string{"in"},
|
||||
},
|
||||
args: defaultArgs,
|
||||
want: []*Task{
|
||||
task30,
|
||||
|
@ -1046,6 +1117,9 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
a: &user.User{ID: 1},
|
||||
},
|
||||
want: []*Task{
|
||||
// The only tasks with a position set
|
||||
task1,
|
||||
task2,
|
||||
// the other ones don't have a position set
|
||||
task3,
|
||||
task4,
|
||||
|
@ -1076,9 +1150,69 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
task31,
|
||||
task32,
|
||||
task33,
|
||||
// The only tasks with a position set
|
||||
task1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "order by due date",
|
||||
fields: fields{
|
||||
SortBy: []string{"due_date", "id"},
|
||||
OrderBy: []string{"asc", "desc"},
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 1},
|
||||
},
|
||||
want: []*Task{
|
||||
// The only tasks with a due date
|
||||
task6,
|
||||
task5,
|
||||
// The other ones don't have a due date
|
||||
task33,
|
||||
task32,
|
||||
task31,
|
||||
task30,
|
||||
task29,
|
||||
task28,
|
||||
task27,
|
||||
task26,
|
||||
task25,
|
||||
task24,
|
||||
task23,
|
||||
task22,
|
||||
task21,
|
||||
task20,
|
||||
task19,
|
||||
task18,
|
||||
task17,
|
||||
task16,
|
||||
task15,
|
||||
task12,
|
||||
task11,
|
||||
task10,
|
||||
task9,
|
||||
task8,
|
||||
task7,
|
||||
task4,
|
||||
task3,
|
||||
task2,
|
||||
task1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "saved filter with sort order",
|
||||
fields: fields{
|
||||
ListID: -2,
|
||||
SortBy: []string{"title", "id"},
|
||||
OrderBy: []string{"desc", "asc"},
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 1},
|
||||
},
|
||||
want: []*Task{
|
||||
task9,
|
||||
task8,
|
||||
task7,
|
||||
task6,
|
||||
task5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -27,16 +27,36 @@ func (tc *TaskComment) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
|||
return t.CanRead(s, a)
|
||||
}
|
||||
|
||||
func (tc *TaskComment) canUserModifyTaskComment(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
t := Task{ID: tc.TaskID}
|
||||
canWriteTask, err := t.CanWrite(s, a)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !canWriteTask {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
savedComment := &TaskComment{
|
||||
ID: tc.ID,
|
||||
TaskID: tc.TaskID,
|
||||
}
|
||||
err = getTaskCommentSimple(s, savedComment)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return a.GetID() == savedComment.AuthorID, nil
|
||||
}
|
||||
|
||||
// CanDelete checks if a user can delete a comment
|
||||
func (tc *TaskComment) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
t := Task{ID: tc.TaskID}
|
||||
return t.CanWrite(s, a)
|
||||
return tc.canUserModifyTaskComment(s, a)
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user can update a comment
|
||||
func (tc *TaskComment) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
t := Task{ID: tc.TaskID}
|
||||
return t.CanWrite(s, a)
|
||||
return tc.canUserModifyTaskComment(s, a)
|
||||
}
|
||||
|
||||
// CanCreate checks if a user can create a new comment
|
||||
|
|
|
@ -151,6 +151,24 @@ func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
|
|||
})
|
||||
}
|
||||
|
||||
func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
|
||||
exists, err := s.
|
||||
Where("id = ? and task_id = ?", tc.ID, tc.TaskID).
|
||||
NoAutoCondition().
|
||||
Get(tc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return ErrTaskCommentDoesNotExist{
|
||||
ID: tc.ID,
|
||||
TaskID: tc.TaskID,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadOne handles getting a single comment
|
||||
// @Summary Remove a task comment
|
||||
// @Description Remove a task comment. The user doing this need to have at least read access to the task this comment belongs to.
|
||||
|
@ -166,15 +184,9 @@ func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/comments/{commentID} [get]
|
||||
func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
exists, err := s.Get(tc)
|
||||
err = getTaskCommentSimple(s, tc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
return ErrTaskCommentDoesNotExist{
|
||||
ID: tc.ID,
|
||||
TaskID: tc.TaskID,
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the author
|
||||
|
|
|
@ -121,6 +121,16 @@ func TestTaskComment_Delete(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskCommentDoesNotExist(err))
|
||||
})
|
||||
t.Run("not the own comment", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{ID: 1, TaskID: 1}
|
||||
can, err := tc.CanDelete(s, &user.User{ID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskComment_Update(t *testing.T) {
|
||||
|
@ -157,6 +167,16 @@ func TestTaskComment_Update(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.True(t, IsErrTaskCommentDoesNotExist(err))
|
||||
})
|
||||
t.Run("not the own comment", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{ID: 1, TaskID: 1}
|
||||
can, err := tc.CanUpdate(s, &user.User{ID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, can)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTaskComment_ReadOne(t *testing.T) {
|
||||
|
@ -167,7 +187,7 @@ func TestTaskComment_ReadOne(t *testing.T) {
|
|||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
tc := &TaskComment{ID: 1}
|
||||
tc := &TaskComment{ID: 1, TaskID: 1}
|
||||
err := tc.ReadOne(s, u)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", tc.Comment)
|
||||
|
|
|
@ -19,40 +19,92 @@ package models
|
|||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, err error) {
|
||||
now = utils.GetTimeWithoutNanoSeconds(now)
|
||||
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[int64]*userWithTasks, err error) {
|
||||
now = utils.GetTimeWithoutSeconds(now)
|
||||
nextMinute := now.Add(1 * time.Minute)
|
||||
|
||||
var tasks []*Task
|
||||
err = s.
|
||||
Where("due_date is not null and due_date < ?", now.Format(dbTimeFormat)).
|
||||
Where("due_date is not null and due_date < ?", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||
And("done = false").
|
||||
Find(&tasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var taskIDs []int64
|
||||
for _, task := range tasks {
|
||||
taskIDs = append(taskIDs, task.ID)
|
||||
}
|
||||
|
||||
return
|
||||
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
uts := make(map[int64]*userWithTasks)
|
||||
tzs := make(map[string]*time.Location)
|
||||
for _, t := range users {
|
||||
if t.User.Timezone == "" {
|
||||
t.User.Timezone = config.GetTimeZone().String()
|
||||
}
|
||||
|
||||
tz, exists := tzs[t.User.Timezone]
|
||||
if !exists {
|
||||
tz, err = time.LoadLocation(t.User.Timezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tzs[t.User.Timezone] = tz
|
||||
}
|
||||
|
||||
// If it is time for that current user, add the task to their list of overdue tasks
|
||||
tm, err := time.Parse("15:04", t.User.OverdueTasksRemindersTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), tm.Hour(), tm.Minute(), 0, 0, tz)
|
||||
isTimeForReminder := overdueMailTime.After(now) || overdueMailTime.Equal(now.In(tz))
|
||||
wasTimeForReminder := overdueMailTime.Before(nextMinute)
|
||||
taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz))
|
||||
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
|
||||
_, exists := uts[t.User.ID]
|
||||
if !exists {
|
||||
uts[t.User.ID] = &userWithTasks{
|
||||
user: t.User,
|
||||
tasks: make(map[int64]*Task),
|
||||
}
|
||||
}
|
||||
uts[t.User.ID].tasks[t.Task.ID] = t.Task
|
||||
}
|
||||
}
|
||||
|
||||
return uts, nil
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -66,36 +118,18 @@ func RegisterOverdueReminderCron() {
|
|||
return
|
||||
}
|
||||
|
||||
err := cron.Schedule("0 8 * * *", func() {
|
||||
err := cron.Schedule("* * * * *", func() {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
now := time.Now()
|
||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
||||
uts, err := getUndoneOverdueTasks(s, now)
|
||||
if err != nil {
|
||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get tasks with reminders in the next minute: %s", err)
|
||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get undone overdue tasks in the next minute: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
|
||||
if err != nil {
|
||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get task users to send them reminders: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
uts := make(map[int64]*userWithTasks)
|
||||
for _, t := range users {
|
||||
_, exists := uts[t.User.ID]
|
||||
if !exists {
|
||||
uts[t.User.ID] = &userWithTasks{
|
||||
user: t.User,
|
||||
tasks: []*Task{},
|
||||
}
|
||||
}
|
||||
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
||||
}
|
||||
|
||||
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users))
|
||||
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(uts))
|
||||
|
||||
for _, ut := range uts {
|
||||
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
||||
|
@ -117,7 +151,6 @@ func RegisterOverdueReminderCron() {
|
|||
}
|
||||
|
||||
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID)
|
||||
continue
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -32,21 +32,34 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
|
|||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
||||
tasks, err := getUndoneOverdueTasks(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 0)
|
||||
assert.Len(t, tasks, 0)
|
||||
})
|
||||
t.Run("undone overdue", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z")
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
||||
uts, err := getUndoneOverdueTasks(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 1)
|
||||
assert.Equal(t, int64(6), taskIDs[0])
|
||||
assert.Len(t, uts, 1)
|
||||
assert.Len(t, uts[1].tasks, 2)
|
||||
// The tasks don't always have the same order, so we only check their presence, not their position.
|
||||
var task5Present bool
|
||||
var task6Present bool
|
||||
for _, t := range uts[1].tasks {
|
||||
if t.ID == 5 {
|
||||
task5Present = true
|
||||
}
|
||||
if t.ID == 6 {
|
||||
task6Present = true
|
||||
}
|
||||
}
|
||||
assert.Truef(t, task5Present, "expected task 5 to be present but was not")
|
||||
assert.Truef(t, task6Present, "expected task 6 to be present but was not")
|
||||
})
|
||||
t.Run("done overdue", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
@ -55,8 +68,8 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
|
|||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
||||
tasks, err := getUndoneOverdueTasks(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 0)
|
||||
assert.Len(t, tasks, 0)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -61,11 +61,11 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
|
|||
// Get all creators of tasks
|
||||
creators := make(map[int64]*user.User, len(taskIDs))
|
||||
err = s.
|
||||
Select("users.id, users.username, users.email, users.name").
|
||||
Select("users.*").
|
||||
Join("LEFT", "tasks", "tasks.created_by_id = users.id").
|
||||
In("tasks.id", taskIDs).
|
||||
Where(cond).
|
||||
GroupBy("tasks.id, users.id, users.username, users.email, users.name").
|
||||
GroupBy("tasks.id, users.id, users.username, users.email, users.name, users.timezone").
|
||||
Find(&creators)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -77,14 +77,14 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
|
|||
return
|
||||
}
|
||||
|
||||
for _, taskID := range taskIDs {
|
||||
u, exists := creators[taskMap[taskID].CreatedByID]
|
||||
for _, task := range taskMap {
|
||||
u, exists := creators[task.CreatedByID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
taskUsers = append(taskUsers, &taskUser{
|
||||
Task: taskMap[taskID],
|
||||
Task: taskMap[task.ID],
|
||||
User: u,
|
||||
})
|
||||
}
|
||||
|
@ -110,8 +110,9 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
|
|||
return
|
||||
}
|
||||
|
||||
func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskIDs []int64, err error) {
|
||||
func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (reminderNotifications []*ReminderDueNotification, err error) {
|
||||
now = utils.GetTimeWithoutNanoSeconds(now)
|
||||
reminderNotifications = []*ReminderDueNotification{}
|
||||
|
||||
nextMinute := now.Add(1 * time.Minute)
|
||||
|
||||
|
@ -120,7 +121,8 @@ func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskI
|
|||
reminders := []*TaskReminder{}
|
||||
err = s.
|
||||
Join("INNER", "tasks", "tasks.id = task_reminders.task_id").
|
||||
Where("reminder >= ? and reminder < ?", now.Format(dbTimeFormat), nextMinute.Format(dbTimeFormat)).
|
||||
// All reminders from -12h to +14h to include all time zones
|
||||
Where("reminder >= ? and reminder < ?", now.Add(time.Hour*-12).Format(dbTimeFormat), nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||
And("tasks.done = false").
|
||||
Find(&reminders)
|
||||
if err != nil {
|
||||
|
@ -133,11 +135,56 @@ func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskI
|
|||
return
|
||||
}
|
||||
|
||||
// We're sending a reminder to everyone who is assigned to the task or has created it.
|
||||
var taskIDs []int64
|
||||
for _, r := range reminders {
|
||||
taskIDs = append(taskIDs, r.TaskID)
|
||||
}
|
||||
|
||||
if len(taskIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
usersWithReminders, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
usersPerTask := make(map[int64][]*taskUser, len(usersWithReminders))
|
||||
for _, ur := range usersWithReminders {
|
||||
usersPerTask[ur.Task.ID] = append(usersPerTask[ur.Task.ID], ur)
|
||||
}
|
||||
|
||||
// Time zone cache per time zone string to avoid parsing the same time zone over and over again
|
||||
tzs := make(map[string]*time.Location)
|
||||
// Figure out which reminders are actually due in the time zone of the users
|
||||
for _, r := range reminders {
|
||||
|
||||
for _, u := range usersPerTask[r.TaskID] {
|
||||
|
||||
if u.User.Timezone == "" {
|
||||
u.User.Timezone = config.GetTimeZone().String()
|
||||
}
|
||||
|
||||
// I think this will break once there's more reminders than what we can handle in one minute
|
||||
tz, exists := tzs[u.User.Timezone]
|
||||
if !exists {
|
||||
tz, err = time.LoadLocation(u.User.Timezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tzs[u.User.Timezone] = tz
|
||||
}
|
||||
|
||||
actualReminder := r.Reminder.In(tz)
|
||||
if (actualReminder.After(now) && actualReminder.Before(now.Add(time.Minute))) || actualReminder.Equal(now) {
|
||||
reminderNotifications = append(reminderNotifications, &ReminderDueNotification{
|
||||
User: u.User,
|
||||
Task: u.Task,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -162,37 +209,26 @@ func RegisterReminderCron() {
|
|||
defer s.Close()
|
||||
|
||||
now := time.Now()
|
||||
taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now)
|
||||
reminders, err := getTasksWithRemindersDueAndTheirUsers(s, now)
|
||||
if err != nil {
|
||||
log.Errorf("[Task Reminder Cron] Could not get tasks with reminders in the next minute: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(taskIDs) == 0 {
|
||||
if len(reminders) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true})
|
||||
if err != nil {
|
||||
log.Errorf("[Task Reminder Cron] Could not get task users to send them reminders: %s", err)
|
||||
return
|
||||
}
|
||||
log.Debugf("[Task Reminder Cron] Sending %d reminders", len(reminders))
|
||||
|
||||
log.Debugf("[Task Reminder Cron] Sending reminders to %d users", len(users))
|
||||
|
||||
for _, u := range users {
|
||||
n := &ReminderDueNotification{
|
||||
User: u.User,
|
||||
Task: u.Task,
|
||||
}
|
||||
|
||||
err = notifications.Notify(u.User, n)
|
||||
for _, n := range reminders {
|
||||
err = notifications.Notify(n.User, n)
|
||||
if err != nil {
|
||||
log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", u.User.ID, err)
|
||||
log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", n.User.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID)
|
||||
log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", n.Task.ID, n.User.ID)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -32,10 +32,10 @@ func TestReminderGetTasksInTheNextMinute(t *testing.T) {
|
|||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now)
|
||||
notifications, err := getTasksWithRemindersDueAndTheirUsers(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 1)
|
||||
assert.Equal(t, int64(27), taskIDs[0])
|
||||
assert.Len(t, notifications, 1)
|
||||
assert.Equal(t, int64(27), notifications[0].Task.ID)
|
||||
})
|
||||
t.Run("Found No Tasks", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
@ -44,7 +44,7 @@ func TestReminderGetTasksInTheNextMinute(t *testing.T) {
|
|||
|
||||
now, err := time.Parse(time.RFC3339Nano, "2018-12-02T01:13:00Z")
|
||||
assert.NoError(t, err)
|
||||
taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now)
|
||||
taskIDs, err := getTasksWithRemindersDueAndTheirUsers(s, now)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, taskIDs, 0)
|
||||
})
|
||||
|
|
|
@ -29,9 +29,11 @@ import (
|
|||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/jinzhu/copier"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
@ -64,7 +66,7 @@ type Task struct {
|
|||
// The list this task belongs to.
|
||||
ListID int64 `xorm:"bigint INDEX not null" json:"list_id" param:"list"`
|
||||
// An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount.
|
||||
RepeatAfter int64 `xorm:"bigint INDEX null" json:"repeat_after"`
|
||||
RepeatAfter int64 `xorm:"bigint INDEX null" json:"repeat_after" valid:"range(0|9223372036854775807)"`
|
||||
// Can have three possible values which will trigger when the task is marked as done: 0 = repeats after the amount specified in repeat_after, 1 = repeats all dates each months (ignoring repeat_after), 3 = repeats from the current date rather than the last set date.
|
||||
RepeatMode TaskRepeatMode `xorm:"not null default 0" json:"repeat_mode"`
|
||||
// The task priority. Can be anything you want, it is possible to sort by this later.
|
||||
|
@ -96,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"`
|
||||
|
||||
|
@ -296,17 +301,20 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
if err := param.validate(); err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
// Mysql sorts columns with null values before ones without null value.
|
||||
// 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 + " " + param.orderBy.String()
|
||||
|
||||
// Postgres sorts by default entries with null values after ones with values.
|
||||
// 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.
|
||||
if db.Type() == schemas.POSTGRES {
|
||||
if param.orderBy == orderAscending {
|
||||
orderby += " NULLS FIRST"
|
||||
}
|
||||
if param.orderBy == orderDescending {
|
||||
orderby += " NULLS LAST"
|
||||
}
|
||||
if db.Type() == schemas.POSTGRES || db.Type() == schemas.SQLITE {
|
||||
orderby += " NULLS LAST"
|
||||
}
|
||||
|
||||
if (i + 1) < len(opts.sortby) {
|
||||
|
@ -333,8 +341,11 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
continue
|
||||
}
|
||||
|
||||
if f.field == "assignees" || f.field == "user_id" {
|
||||
f.field = "user_id"
|
||||
if f.field == "assignees" {
|
||||
if f.comparator == taskFilterComparatorLike {
|
||||
return nil, 0, 0, ErrInvalidTaskFilterValue{Field: f.field, Value: f.value}
|
||||
}
|
||||
f.field = "username"
|
||||
filter, err := getFilterCond(f, opts.filterIncludeNulls)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
|
@ -402,7 +413,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
userListIDs := make([]int64, len(userLists))
|
||||
userListIDs := make([]int64, 0, len(userLists))
|
||||
for _, l := range userLists {
|
||||
userListIDs = append(userListIDs, l.ID)
|
||||
}
|
||||
|
@ -425,7 +436,13 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
|
|||
}
|
||||
|
||||
if len(assigneeFilters) > 0 {
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_assignees", opts.filterConcat, assigneeFilters))
|
||||
assigneeFilter := []builder.Cond{
|
||||
builder.In("user_id",
|
||||
builder.Select("id").
|
||||
From("users").
|
||||
Where(builder.Or(assigneeFilters...)),
|
||||
)}
|
||||
filters = append(filters, getFilterCondForSeparateTable("task_assignees", opts.filterConcat, assigneeFilter))
|
||||
}
|
||||
|
||||
if len(labelFilters) > 0 {
|
||||
|
@ -673,7 +690,17 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
|
|||
continue
|
||||
}
|
||||
fullRelatedTasks[rt.OtherTaskID].IsFavorite = taskFavorites[rt.OtherTaskID]
|
||||
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], fullRelatedTasks[rt.OtherTaskID])
|
||||
|
||||
// We're duplicating the other task to avoid cycles as these can't be represented properly in json
|
||||
// and would thus fail with an error.
|
||||
otherTask := &Task{}
|
||||
err = copier.Copy(otherTask, fullRelatedTasks[rt.OtherTaskID])
|
||||
if err != nil {
|
||||
log.Errorf("Could not duplicate task object: %v", err)
|
||||
continue
|
||||
}
|
||||
otherTask.RelatedTasks = nil
|
||||
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], otherTask)
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -883,7 +910,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
|
||||
// Generate a uuid if we don't already have one
|
||||
if t.UID == "" {
|
||||
t.UID = utils.MakeRandomString(40)
|
||||
t.UID = uuid.NewString()
|
||||
}
|
||||
|
||||
// Get the default bucket and move the task there
|
||||
|
@ -955,6 +982,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) {
|
||||
|
||||
|
@ -1014,6 +1042,7 @@ 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
|
||||
|
@ -1028,6 +1057,23 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
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
|
||||
|
@ -1123,6 +1169,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...).
|
||||
|
@ -1265,18 +1315,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1286,8 +1357,8 @@ 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
|
||||
// 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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ type TeamMember struct {
|
|||
}
|
||||
|
||||
// TableName makes beautiful table names
|
||||
func (TeamMember) TableName() string {
|
||||
func (*TeamMember) TableName() string {
|
||||
return "team_members"
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue