forked from vikunja/vikunja
Compare commits
160 Commits
release/0.
...
main
Author | SHA1 | Date |
---|---|---|
renovate | abfdae0012 | |
renovate | 5f5936c972 | |
renovate | 111ac92619 | |
kolaente | bf00ea4939 | |
kolaente | 465f6d90ab | |
renovate | 07bda3eae3 | |
kolaente | 03d818fd9f | |
kolaente | f019ae42bb | |
kolaente | 9000f2c3cd | |
kolaente | cc1bb3083f | |
kolaente | be47459c14 | |
renovate | 086f005e3d | |
kolaente | 24bb7e98fa | |
kolaente | 496c38de8e | |
konrad | 90146aea5b | |
renovate | fc51a3e76f | |
renovate | 4154247d95 | |
renovate | e3956527d4 | |
kolaente | 15e5a9069b | |
kolaente | c9d0f519ee | |
renovate | 72f7c3333a | |
kolaente | d7b74500c3 | |
kolaente | bd24743640 | |
renovate | e2dd9d85b5 | |
kolaente | f8833ae8a2 | |
renovate | ade085ddeb | |
renovate | e00bcf4802 | |
renovate | aee4b14ed0 | |
renovate | f98c7877fc | |
renovate | 52eb923327 | |
renovate | f04c8048df | |
renovate | 8382d06ea9 | |
renovate | 67681f9e2b | |
renovate | fef3d8199c | |
renovate | 32ba088497 | |
renovate | b451399a1d | |
kolaente | 98f367eb97 | |
kolaente | dcddaab7b5 | |
renovate | b3bb7395cd | |
kolaente | 483496cc26 | |
renovate | 7ffa08ee1a | |
renovate | ed65a7cd34 | |
renovate | 83809d1bd6 | |
konrad | 2007f63502 | |
kolaente | b92da0a5f1 | |
renovate | 6b7aa380ec | |
konrad | 27119ad6d4 | |
renovate | cd21c5fc6e | |
kolaente | 77c2b77079 | |
renovate | 1fbd9b67e0 | |
renovate | fb69260b64 | |
renovate | aeba72a7ea | |
renovate | cd5b46363d | |
renovate | c3da454854 | |
renovate | e38be9bd18 | |
kolaente | dc2915875b | |
renovate | be4d53d805 | |
kolaente | d34c85d544 | |
kolaente | 647f3cb9f1 | |
kolaente | f237afd2ac | |
konrad | 4c5f457313 | |
kolaente | 9c2a59582a | |
kolaente | d746c1bede | |
kolaente | 220f43331f | |
kolaente | 24f7d9b4f7 | |
kolaente | 5cfc9bf2f9 | |
kolaente | 3572ac4b82 | |
konrad | 1571dfa825 | |
kolaente | e600f61e06 | |
renovate | ea921f5350 | |
konrad | 6ccb85a0dc | |
kolaente | dac315db59 | |
kolaente | eae3cbc7bb | |
kolaente | d9b38b85f6 | |
kolaente | c7f337f303 | |
kolaente | 733f26f017 | |
kolaente | e4a0066e20 | |
kolaente | d28390d792 | |
kolaente | 0b90d826be | |
renovate | 850c3a3dd4 | |
kolaente | 4cf7c459da | |
kolaente | 2a80e552cc | |
kolaente | 7e229a1b83 | |
kolaente | 7ee535de47 | |
konrad | 4216ed7277 | |
kolaente | d5d4d8b6ed | |
kolaente | 9559cbf1ec | |
kolaente | 265e778867 | |
renovate | 53f37e12ec | |
renovate | 302199089d | |
kolaente | 7adbd21698 | |
kolaente | d26f81162f | |
kolaente | e21a3904ff | |
kolaente | 562ef9af36 | |
renovate | 294fc16593 | |
konrad | d0c77ad1c1 | |
kolaente | 373e3f3d60 | |
kolaente | 358661e060 | |
kolaente | 32a07c4c61 | |
kolaente | 5b825f1cc8 | |
andreymal | 50b49ffab6 | |
kolaente | 8b6aeb8571 | |
kolaente | 0c5dfe5c48 | |
kolaente | d7932d2648 | |
renovate | f1df45b632 | |
renovate | ff2dac1a10 | |
renovate | e857d73c22 | |
renovate | b3f7827e39 | |
renovate | bba6d8feb1 | |
renovate | 2cc365ff0c | |
renovate | 5593d7bace | |
renovate | e6d4620f92 | |
renovate | f70d357a93 | |
kolaente | 37718c3282 | |
kolaente | 7408380560 | |
renovate | 4ed4a96059 | |
renovate | 9b03c28e0d | |
renovate | f17e2f9e7c | |
renovate | 441e38086d | |
kolaente | bf68ccbb25 | |
renovate | 1386ed4023 | |
renovate | d2a3db5dce | |
kolaente | 63340b32f8 | |
renovate | 52f6b10a65 | |
renovate | 81d469f687 | |
renovate | 769b5d1c6c | |
renovate | 1c77e40e6e | |
kolaente | 6f54d4d8be | |
renovate | 6018573d81 | |
renovate | b4f08e88ae | |
renovate | 88c3bd43a4 | |
renovate | 61dd159131 | |
renovate | 5c79add056 | |
renovate | 6b70069eba | |
kolaente | 9147e6739f | |
kolaente | 570d146b21 | |
renovate | cc2c158b9d | |
kolaente | 78a206c818 | |
kolaente | fc5703ac8c | |
sytone | 3277f6acf7 | |
kolaente | 8a1e98a7f2 | |
kolaente | 9a2655dbf1 | |
kolaente | d48aa101cf | |
kolaente | df45675df3 | |
kolaente | afd6bde74d | |
kolaente | e23014dbe4 | |
kolaente | 8e65ffb99b | |
kolaente | 3f6d85497f | |
kolaente | 88b9ea6a96 | |
renovate | 67f863120e | |
kolaente | ffede02ddc | |
kolaente | 3973ce985d | |
renovate | 2351194547 | |
kolaente | b7ec24ff52 | |
renovate | aaac4c6dfc | |
kolaente | 2e52cc1802 | |
kolaente | 20ede346b4 | |
kolaente | b76ad8efe2 | |
renovate | d695681a0e | |
renovate | 52c3075c3d |
|
@ -1,3 +1,4 @@
|
|||
---
|
||||
kind: pipeline
|
||||
name: testing
|
||||
|
||||
|
@ -5,37 +6,91 @@ workspace:
|
|||
base: /go
|
||||
path: src/code.vikunja.io/api
|
||||
|
||||
volumes:
|
||||
- name: tmp-sqlite-unit
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-sqlite-integration
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-sqlite-migration
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-mysql-unit
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-mysql-integration
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-mysql-migration
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-postgres-unit
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-postgres-integration
|
||||
temp:
|
||||
medium: memory
|
||||
- name: tmp-postgres-migration
|
||||
temp:
|
||||
medium: memory
|
||||
|
||||
|
||||
services:
|
||||
- name: test-mysql-unit
|
||||
image: mariadb:10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: vikunjatest
|
||||
MYSQL_DATABASE: vikunjatest
|
||||
volumes:
|
||||
- name: tmp-mysql-unit
|
||||
path: /var/lib/mysql
|
||||
- name: test-mysql-integration
|
||||
image: mariadb:10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: vikunjatest
|
||||
MYSQL_DATABASE: vikunjatest
|
||||
volumes:
|
||||
- name: tmp-mysql-integration
|
||||
path: /var/lib/mysql
|
||||
- name: test-mysql-migration
|
||||
image: mariadb:10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: vikunjatest
|
||||
MYSQL_DATABASE: vikunjatest
|
||||
volumes:
|
||||
- name: tmp-mysql-migration
|
||||
path: /var/lib/mysql
|
||||
- name: test-postgres-unit
|
||||
image: postgres:12
|
||||
image: postgres:13
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
volumes:
|
||||
- name: tmp-postgres-unit
|
||||
path: /var/lib/postgresql/data
|
||||
commands:
|
||||
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
|
||||
- name: test-postgres-integration
|
||||
image: postgres:12
|
||||
image: postgres:13
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
volumes:
|
||||
- name: tmp-postgres-integration
|
||||
path: /var/lib/postgresql/data
|
||||
commands:
|
||||
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
|
||||
- name: test-postgres-migration
|
||||
image: postgres:12
|
||||
image: postgres:13
|
||||
environment:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
volumes:
|
||||
- name: tmp-postgres-migration
|
||||
path: /var/lib/postgresql/data
|
||||
commands:
|
||||
- docker-entrypoint.sh -c fsync=off -c full_page_writes=off # turns of wal
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
|
@ -102,9 +157,12 @@ steps:
|
|||
depends_on: [ test-migration-prepare, build ]
|
||||
environment:
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
VIKUNJA_DATABASE_PATH: ./vikunja-migration-test.db
|
||||
VIKUNJA_DATABASE_PATH: /db/vikunja-migration-test.db
|
||||
VIKUNJA_LOG_DATABASE: stdout
|
||||
VIKUNJA_LOG_DATABASELEVEL: debug
|
||||
volumes:
|
||||
- name: tmp-sqlite-migration
|
||||
path: /db
|
||||
commands:
|
||||
- ./vikunja-unstable-linux-amd64 migrate
|
||||
# Run the migrations from the binary build in the step before
|
||||
|
@ -169,6 +227,10 @@ steps:
|
|||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
VIKUNJA_DATABASE_PATH: /db/vikunja-test.db
|
||||
volumes:
|
||||
- name: tmp-sqlite-unit
|
||||
path: /db
|
||||
commands:
|
||||
- ./mage-static test:unit
|
||||
depends_on: [ fetch-tags, mage ]
|
||||
|
@ -228,6 +290,10 @@ steps:
|
|||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
VIKUNJA_DATABASE_PATH: /db/vikunja-test.db
|
||||
volumes:
|
||||
- name: tmp-sqlite-integration
|
||||
path: /db
|
||||
commands:
|
||||
- ./mage-static test:integration
|
||||
depends_on: [ fetch-tags, mage ]
|
||||
|
@ -555,7 +621,7 @@ steps:
|
|||
- tar -xzf vikunja-theme.tar.gz
|
||||
|
||||
- name: build
|
||||
image: monachus/hugo:v0.54.0
|
||||
image: monachus/hugo:v0.75.1
|
||||
pull: true
|
||||
commands:
|
||||
- cd docs
|
||||
|
@ -596,7 +662,7 @@ steps:
|
|||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
- name: docker-arm-latest
|
||||
- name: docker-arm-unstable
|
||||
image: plugins/docker:linux-arm
|
||||
pull: true
|
||||
settings:
|
||||
|
@ -605,7 +671,7 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: latest-linux-arm
|
||||
tags: unstable-linux-arm
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
|
@ -627,7 +693,7 @@ steps:
|
|||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
- name: docker-arm64-latest
|
||||
- name: docker-arm64-unstable
|
||||
image: plugins/docker:linux-arm64
|
||||
pull: true
|
||||
settings:
|
||||
|
@ -636,7 +702,7 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: latest-linux-arm64
|
||||
tags: unstable-linux-arm64
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
|
@ -680,7 +746,8 @@ steps:
|
|||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
- name: docker-latest
|
||||
|
||||
- name: docker-unstable
|
||||
image: plugins/docker:linux-amd64
|
||||
pull: true
|
||||
settings:
|
||||
|
@ -689,7 +756,7 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/api
|
||||
tags: latest-linux-amd64
|
||||
tags: unstable-linux-amd64
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
|
@ -726,13 +793,13 @@ depends_on:
|
|||
- docker-arm-release
|
||||
|
||||
steps:
|
||||
- name: manifest-latest
|
||||
- name: manifest-unstable
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
tags: latest
|
||||
tags: unstable
|
||||
ignore_missing: true
|
||||
spec: docker-manifest-latest.tmpl
|
||||
spec: docker-manifest-unstable.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
|
@ -741,7 +808,7 @@ steps:
|
|||
ref:
|
||||
- refs/heads/main
|
||||
|
||||
- name: manifest
|
||||
- name: manifest-release
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
|
@ -756,6 +823,23 @@ steps:
|
|||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
- name: manifest-release-latest
|
||||
pull: always
|
||||
image: plugins/manifest
|
||||
depends_on:
|
||||
- clone
|
||||
settings:
|
||||
tags: latest
|
||||
ignore_missing: true
|
||||
spec: docker-manifest.tmpl
|
||||
password:
|
||||
from_secret: docker_password
|
||||
username:
|
||||
from_secret: docker_username
|
||||
when:
|
||||
ref:
|
||||
- "refs/tags/**"
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
|
@ -788,3 +872,8 @@ steps:
|
|||
status:
|
||||
- success
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 110b782e9b704b4b3b3d618678383718c92262cf3c214f4fe6705d40cd3da367
|
||||
|
||||
...
|
|
@ -0,0 +1,22 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[*.{yaml,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 4
|
191
CHANGELOG.md
191
CHANGELOG.md
|
@ -2,14 +2,181 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
|
||||
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
All releases can be found on https://code.vikunja.io/api/releases.
|
||||
|
||||
## [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
|
||||
|
||||
* Add default list setting (#875)
|
||||
* Add menu link to Vikunja Cloud in docs
|
||||
* Add more logging and better error messages for openid authentication + clarify docs
|
||||
* Add more logging for test data api endpoint
|
||||
* Add searching for tasks by index
|
||||
* Add setting for first day of the week
|
||||
* Add support of Unix socket (#912)
|
||||
* Add truncate parameter to test fixtures setup
|
||||
* Notify the user after three failed login attempts
|
||||
* Reorder tasks, lists and kanban buckets (#923)
|
||||
* Send a notification on failed TOTP
|
||||
* Task mentions (#926)
|
||||
* Try to get more information about the user when authenticating with openid
|
||||
* User account deletion (#937)
|
||||
* User Data Export and import (#967)
|
||||
|
||||
### Changed
|
||||
|
||||
* Allow running migration 20210711173657 multiple times to fix issues when it didn't completely run through previously
|
||||
* Better logging for errors while importing a bunch of tasks
|
||||
* Change task title to TEXT instead of varchar(250) to allow for longer task titles
|
||||
* Disable the user account after 10 failed password attempts
|
||||
* Docs: Add a note about default password
|
||||
* Docs: Add another youtube tutorial
|
||||
* Docs: Add ios to the list of not working caldav clients
|
||||
* Docs: Add k8s-at-home Helm Chart for Vikunja
|
||||
* Docs: Add other installation resources
|
||||
* Docs: Add translation docs
|
||||
* Docs: Fix rewrite rules in apache example configs
|
||||
* Docs: Translation now happening at crowdin
|
||||
* Docs: Update translation guidelines
|
||||
* Don't fail when removing the last bucket in migration from other services
|
||||
* Don't notify the user who created the team
|
||||
* Don't use the mariadb root user in docker-compose examples
|
||||
* Ensure case insensitive search on postgres (#927)
|
||||
* Increase test timeout
|
||||
* Only filter out failing openid providers if multiple are configured and one of them failed
|
||||
* Only send an email about failed totp after three failed attempts
|
||||
* Rearrange setting frontend url in config
|
||||
* Refactor user email confirmation + password reset handling (#919)
|
||||
* Rename and sign drone config
|
||||
* Replace jwt-go with github.com/golang-jwt/jwt
|
||||
* Reset failed totp attempts when logging in successfully
|
||||
* Save user tokens as text and not varchar
|
||||
* Save user tokens as varchar(450) and not text to fix mysql indexing issues
|
||||
* Set todoist migration redirect url to the frontend url by default
|
||||
* Show config full paths and env variables with config options
|
||||
* Switch the :latest docker image tag to contain the latest release instead of the latest unstable
|
||||
* Tune test db server settings to speed up tests (#939)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fix authentication callback
|
||||
* Fix duplicating empty lists
|
||||
* Fix error handling when deleting an attachment file
|
||||
* Fix error when searching for a namespace returned no results
|
||||
* Fix error when searching for a namespace with subscribers
|
||||
* Fix goimports
|
||||
* Fix importing archived projects and done items from todoist
|
||||
* Fix jwt middleware
|
||||
* Fix lint
|
||||
* Fix mapping task priorities from Vikunja to calDAV
|
||||
* Fix moving the done bucket around
|
||||
* Fix old references to master in docs
|
||||
* Fix panic on invalid smtp config
|
||||
* Fix parsing openid config when using a json config file
|
||||
* Fix saving pointer values to memory keyvalue
|
||||
* Fix saving reminders of repeating tasks
|
||||
* Fix setting a saved filter as favorite
|
||||
* Fix setting task favorite status of related tasks
|
||||
* Fix setting up keyvalue storage in tests
|
||||
* Fix swagger docs for create requests
|
||||
* Fix task relations not getting properly cleaned up when deleting them
|
||||
* Fix tests & lint
|
||||
* Make sure a bucket exists or use the default bucket when importing tasks
|
||||
* Make sure all associated entities of a task are deleted when the task is deleted
|
||||
* Make sure list / task favorites are set per user, not per entity (#915)
|
||||
* Make sure the configured frontend url always has a / at the end
|
||||
* Refactor & fix storing struct-values in redis keyvalue
|
||||
* Todoist migration: don't panic if no reminder was found for task
|
||||
|
||||
### Dependency updates
|
||||
|
||||
* fix(deps): update golang.org/x/sys commit hash to 63515b4 (#959)
|
||||
* fix(deps): update golang.org/x/sys commit hash to 97244b9 (#965)
|
||||
* fix(deps): update golang.org/x/sys commit hash to f475640 (#962)
|
||||
* fix(deps): update golang.org/x/sys commit hash to f4d4317 (#961)
|
||||
* fix(deps): update module github.com/lib/pq to v1.10.3 (#963)
|
||||
* Update alpine Docker tag to v3.13 (#884)
|
||||
* Update alpine Docker tag to v3.14 (#889)
|
||||
* Update golang.org/x/crypto commit hash to 0a44fdf (#944)
|
||||
* Update golang.org/x/crypto commit hash to 0ba0e8f (#943)
|
||||
* Update golang.org/x/crypto commit hash to 32db794 (#949)
|
||||
* Update golang.org/x/crypto commit hash to 5ff15b2 (#891)
|
||||
* Update golang.org/x/crypto commit hash to a769d52 (#916)
|
||||
* Update golang.org/x/image commit hash to 775e3b0 (#880)
|
||||
* Update golang.org/x/image commit hash to a66eb64 (#900)
|
||||
* Update golang.org/x/image commit hash to e6eecd4 (#893)
|
||||
* Update golang.org/x/net commit hash to 37e1c6af
|
||||
* Update golang.org/x/oauth2 commit hash to 14747e6 (#894)
|
||||
* Update golang.org/x/oauth2 commit hash to 2bc19b1 (#955)
|
||||
* Update golang.org/x/oauth2 commit hash to 6f1e639 (#931)
|
||||
* Update golang.org/x/oauth2 commit hash to 7df4dd6 (#952)
|
||||
* Update golang.org/x/oauth2 commit hash to a41e5a7 (#902)
|
||||
* Update golang.org/x/oauth2 commit hash to a8dc77f (#896)
|
||||
* Update golang.org/x/oauth2 commit hash to bce0382 (#895)
|
||||
* Update golang.org/x/oauth2 commit hash to d040287 (#888)
|
||||
* Update golang.org/x/oauth2 commit hash to f6687ab (#862)
|
||||
* Update golang.org/x/oauth2 commit hash to faf39c7 (#935)
|
||||
* Update golang.org/x/sys commit hash to 00dd8d7 (#953)
|
||||
* Update golang.org/x/sys commit hash to 15123e1 (#946)
|
||||
* Update golang.org/x/sys commit hash to 1e6c022 (#947)
|
||||
* Update golang.org/x/sys commit hash to 30e4713 (#945)
|
||||
* Update golang.org/x/sys commit hash to 41cdb87 (#956)
|
||||
* Update golang.org/x/sys commit hash to 7d9622a (#948)
|
||||
* Update golang.org/x/sys commit hash to bfb29a6 (#951)
|
||||
* Update golang.org/x/sys commit hash to d867a43 (#934)
|
||||
* Update golang.org/x/sys commit hash to e5e7981 (#933)
|
||||
* Update golang.org/x/sys commit hash to f52c844 (#954)
|
||||
* Update golang.org/x/term commit hash to 6886f2d (#887)
|
||||
* Update module getsentry/sentry-go to v0.11.0 (#869)
|
||||
* Update module github.com/coreos/go-oidc to v3 (#885)
|
||||
* Update module github.com/gabriel-vasile/mimetype to v1.3.1 (#904)
|
||||
* Update module github.com/golang-jwt/jwt to v3.2.2 (#928)
|
||||
* Update module github.com/golang-jwt/jwt to v4 (#930)
|
||||
* Update module github.com/go-redis/redis/v8 to v8.11.0 (#903)
|
||||
* Update module github.com/go-redis/redis/v8 to v8.11.1 (#925)
|
||||
* Update module github.com/go-redis/redis/v8 to v8.11.2 (#932)
|
||||
* Update module github.com/go-redis/redis/v8 to v8.11.3 (#942)
|
||||
* Update module github.com/iancoleman/strcase to v0.2.0 (#918)
|
||||
* Update module github.com/labstack/echo/v4 to v4.4.0 (#917)
|
||||
* Update module github.com/labstack/echo/v4 to v4.5.0 (#929)
|
||||
* Update module github.com/mattn/go-sqlite3 to v1.14.8 (#921)
|
||||
* Update module github.com/spf13/cobra to v1.2.0 (#905)
|
||||
* Update module github.com/spf13/cobra to v1.2.1 (#906)
|
||||
* Update module github.com/spf13/viper to v1.8.0 (#890)
|
||||
* Update module github.com/spf13/viper to v1.8.1 (#899)
|
||||
* Update module github.com/swaggo/swag to v1.7.1 (#936)
|
||||
* Update module github.com/yuin/goldmark to v1.3.8 (#892)
|
||||
* Update module github.com/yuin/goldmark to v1.3.9 (#901)
|
||||
* Update module github.com/yuin/goldmark to v1.4.0 (#908)
|
||||
* Update module go-redis/redis/v8 to v8.10.0 (#882)
|
||||
* Update module go-redis/redis/v8 to v8.7.1 (#807)
|
||||
* Update module go-testfixtures/testfixtures/v3 to v3.6.1 (#868)
|
||||
* Update module lib/pq to v1.10.2 (#865)
|
||||
* Update module prometheus/client_golang to v1.11.0 (#879)
|
||||
* Update module yuin/goldmark to v1.3.6 (#863)
|
||||
* Update module yuin/goldmark to v1.3.7 (#867)
|
||||
* Update monachus/hugo Docker tag to v0.75.1 (#940)
|
||||
|
||||
## [0.17.1] - 2021-06-09
|
||||
|
||||
### Fixed
|
||||
### Fixed
|
||||
|
||||
* Fix parsing openid config when using a json config file
|
||||
|
||||
|
@ -452,7 +619,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
|
||||
## [0.14.0] - 2020-07-01
|
||||
|
||||
### Added
|
||||
### Added
|
||||
|
||||
* Add ability to run the docker container with configurable user and group ids
|
||||
* Add better errors if the sqlite db file is not writable
|
||||
|
@ -666,7 +833,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
|
||||
## [0.12] - 2020-04-04
|
||||
|
||||
#### Added
|
||||
#### Added
|
||||
|
||||
* Add support for archiving lists and namespaces (#152)
|
||||
* Colors for lists and namespaces (#155)
|
||||
|
@ -690,7 +857,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
|
||||
## [0.11] - 2020-03-01
|
||||
|
||||
### Added
|
||||
### Added
|
||||
|
||||
* Add config options for cors handling (#124)
|
||||
* Add config options for task attachments (#125)
|
||||
|
@ -758,7 +925,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
|
||||
## [0.9] - 2019-11-24
|
||||
|
||||
### Added
|
||||
### Added
|
||||
|
||||
* Task Attachments (#104)
|
||||
* Task Relations (#103)
|
||||
|
@ -771,7 +938,8 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
### Fixed
|
||||
|
||||
* Fix default logging settings (#107)
|
||||
* Fixed a bug where adding assignees or reminders via an update would re-create them and not respect already inserted ones, leaving a lot of garbage
|
||||
* Fixed a bug where adding assignees or reminders via an update would re-create them and not respect already inserted
|
||||
ones, leaving a lot of garbage
|
||||
* Fixed a bug where deleting an attachment would cause a nil panic
|
||||
* Fixed building docs theme
|
||||
* Fixed error when setting max file size on 32-Bit systems
|
||||
|
@ -784,7 +952,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
* Fixed removing reminders
|
||||
* Small link share fixes (#96)
|
||||
|
||||
### Changed
|
||||
### Changed
|
||||
|
||||
* Improve pagination (#105)
|
||||
* Moved `teams_{namespace|list}_*` to `{namespace|list}_teams_*` for better consistency (#101)
|
||||
|
@ -905,7 +1073,8 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
|
||||
* Updated libraries
|
||||
* Updated drone to version 1
|
||||
* Releases are now signed with our pgp key (more info about this on [the download page](https://vikunja.io/en/download/)).
|
||||
* Releases are now signed with our pgp key (more info about this
|
||||
on [the download page](https://vikunja.io/en/download/)).
|
||||
|
||||
## [0.5] - 2018-12-02
|
||||
|
||||
|
@ -935,7 +1104,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
|
|||
|
||||
## [0.3] - 2018-11-02
|
||||
|
||||
### Added
|
||||
### Added
|
||||
|
||||
* Password reset
|
||||
* Email verification when registering
|
||||
|
|
|
@ -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.17.1-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.18.1-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)
|
||||
|
|
|
@ -5,6 +5,10 @@ service:
|
|||
JWTSecret: "<jwt-secret>"
|
||||
# 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
|
||||
unixsocket:
|
||||
# Permission bits for the Unix socket. Note that octal values must be prefixed by "0o", e.g. 0o660
|
||||
unixsocketmode:
|
||||
# The URL of the frontend, used to send password reset emails.
|
||||
frontendurl: ""
|
||||
# The base path on the file system where the binary and assets are.
|
||||
|
@ -39,6 +43,10 @@ service:
|
|||
# If enabled, vikunja will send an email to everyone who is either assigned to a task or created it when a task reminder
|
||||
# is due.
|
||||
enableemailreminders: true
|
||||
# If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
# 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
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
|
@ -190,7 +198,7 @@ migration:
|
|||
# This is usually the frontend url where the frontend then makes a request to /migration/todoist/migrate
|
||||
# with the code obtained from the todoist api.
|
||||
# Note that the vikunja frontend expects this to be /migrate/todoist
|
||||
redirecturl:
|
||||
redirecturl: <frontend url>/migrate/todoist
|
||||
trello:
|
||||
# Wheter to enable the trello migrator or not
|
||||
enable: false
|
||||
|
@ -262,10 +270,12 @@ auth:
|
|||
enabled: true
|
||||
# OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
|
||||
# The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
|
||||
# **Note:** The frontend expects to be redirected after authentication by the third party
|
||||
# **Note:** Some openid providers (like gitlab) only make the email of the user available through openid claims if they have set it to be publicly visible.
|
||||
# If the email is not public in those cases, authenticating will fail.
|
||||
# **Note 2:** The frontend expects to be redirected after authentication by the third party
|
||||
# to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url with your third party
|
||||
# auth service accordingy if you're using the default vikunja frontend.
|
||||
# Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) for more information about how to configure openid authentication.
|
||||
# Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
openid:
|
||||
# Enable or disable OpenID Connect authentication
|
||||
enabled: false
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
image: vikunja/api:latest
|
||||
image: vikunja/api:unstable
|
||||
manifests:
|
||||
-
|
||||
image: vikunja/api:latest-linux-amd64
|
||||
image: vikunja/api:unstable-linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/api:latest-linux-arm64
|
||||
image: vikunja/api:unstable-linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
-
|
||||
image: vikunja/api:latest-linux-arm
|
||||
image: vikunja/api:unstable-linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
|
@ -31,10 +31,10 @@ menu:
|
|||
url: https://vikunja.io/en/
|
||||
weight: 10
|
||||
- name: Features
|
||||
url: https://vikunja.io/en/features
|
||||
url: https://vikunja.io/features
|
||||
weight: 20
|
||||
- name: Download
|
||||
url: https://vikunja.io/en/download
|
||||
url: https://vikunja.io/download
|
||||
weight: 30
|
||||
- name: Docs
|
||||
url: https://vikunja.io/docs
|
||||
|
@ -45,3 +45,6 @@ menu:
|
|||
- name: Community
|
||||
url: https://community.vikunja.io/
|
||||
weight: 60
|
||||
- name: Get it Hosted
|
||||
url: https://vikunja.cloud/?utm_source=io&utm_medium=io&utm_campaign=menu
|
||||
weight: 70
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
date: "2019-03-31:00:00+01:00"
|
||||
title: "Adding new cli commands"
|
||||
title: "Cli Commands"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Configuration Options"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Configuration options
|
||||
|
||||
All configuration variables are declared in the `config` package.
|
||||
It uses [viper](https://github.com/spf13/viper) under the hood to handle setting defaults and parsing config files.
|
||||
Viper handles parsing all different configuration sources.
|
||||
|
||||
## Adding new config options
|
||||
|
||||
To make handling configuration parameters a bit easier, we introduced a `Key` string type in the `config` package which
|
||||
you can call directly to get a config value.
|
||||
|
||||
To add a new config option, you should add a new key const to `pkg/config/config.go` and possibly a default value.
|
||||
Default values should always enable the feature to work or turn it off completely if it always needs
|
||||
additional configuration.
|
||||
|
||||
Make sure to also add the new config option to the default config file (`config.yml.sample` at the root of the repository)
|
||||
with an explanatory comment to make sure it is well documented.
|
||||
Then run `mage generate-docs` to generate the configuration docs from the sample file.
|
||||
|
||||
## Getting Configuration Values
|
||||
|
||||
To retreive a configured value call the key with a getter for the type you need.
|
||||
For example:
|
||||
|
||||
{{< highlight golang >}}
|
||||
if config.CacheEnabled.GetBool() {
|
||||
// Do something with enabled caches
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
Take a look at the methods declared on the type to see what's available.
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
title: "Cron Tasks"
|
||||
date: 2021-07-13T23:21:52+02:00
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# How to add a cron job task
|
||||
|
||||
Cron jobs are tasks which run on a predefined schedule.
|
||||
Vikunja uses these through a light wrapper package around the excellent [github.com/robfig/cron](https://github.com/robfig/cron) package.
|
||||
|
||||
The package exposes a `cron.Schedule` method with two arguments: The first one to define the schedule when the cron task
|
||||
should run, and the second one with the actual function to run at the schedule.
|
||||
You would then create a new function to register your the actual cron task in your package.
|
||||
|
||||
A basic function to register a cron task looks like this:
|
||||
|
||||
{{< highlight golang >}}
|
||||
func RegisterSomeCronTask() {
|
||||
err := cron.Schedule("0 * * * *", func() {
|
||||
// Do something every hour
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
Call the register method in the `FullInit()` method of the `init` package to actually register it.
|
||||
|
||||
## Schedule Syntax
|
||||
|
||||
The cron syntax uses the same on you may know from unix systems.
|
||||
|
||||
It is described in detail [here](https://pkg.go.dev/github.com/robfig/cron#hdr-CRON_Expression_Format).
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Database"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Using the database
|
||||
|
||||
When using the common web handlers, you get an `xorm.Session` to do database manipulations.
|
||||
In other packages, use the `db.NewSession()` method to get a new database session.
|
||||
|
||||
## Adding new database tables
|
||||
|
||||
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/).
|
||||
|
||||
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.
|
||||
|
||||
## Adding data to test fixtures
|
||||
|
||||
Adding data for test fixtures can be done via `yaml` files in `pkg/models/fixtures`.
|
||||
|
||||
The name of the yaml file should match the table name in the database.
|
||||
Adding values to it is done via array definition inside it.
|
||||
|
||||
**Note**: Table and column names need to be in snake_case as that's what is used internally in the database
|
||||
and for mapping values from the database to xorm so your structs can use it.
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
date: "2019-03-29:00:00+02:00"
|
||||
title: "Database migrations"
|
||||
title: "Database Migrations"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
|
@ -37,6 +37,11 @@ All migrations are sorted before being executed, since `init()` does not guarant
|
|||
When you're adding a new struct, you also need to add it to the `models.GetTables()` function
|
||||
to ensure it will be created on new installations.
|
||||
|
||||
### Generating a new migration stub
|
||||
|
||||
You can easily generate a pre-filled migration stub by running `mage dev:make-migration`.
|
||||
It will ask you for a table name and generate an empty migration similar to the example shown below.
|
||||
|
||||
### Example
|
||||
|
||||
{{< highlight golang >}}
|
||||
|
|
|
@ -12,56 +12,11 @@ menu:
|
|||
|
||||
# Development
|
||||
|
||||
We use go modules to vendor libraries for Vikunja, so you'll need at least go `1.11` to use these.
|
||||
If you don't intend to add new dependencies, go `1.9` and above should be fine.
|
||||
We use go modules to manage third-party libraries for Vikunja, so you'll need at least go `1.11` to use these.
|
||||
|
||||
To contribute to Vikunja, fork the project and work on the master branch.
|
||||
To contribute to Vikunja, fork the project and work on the main branch.
|
||||
|
||||
A lot of developing tasks are automated using a Magefile, so make sure to [take a look at it]({{< ref "mage.md">}}).
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Libraries
|
||||
|
||||
We keep all libraries used for Vikunja around in the `vendor/` folder to still be able to build the project even if
|
||||
some maintainers take their libraries down like [it happened in the past](https://github.com/jteeuwen/go-bindata/issues/5).
|
||||
|
||||
## Tests
|
||||
|
||||
See [testing]({{< ref "test.md">}}).
|
||||
|
||||
#### Development using go modules
|
||||
|
||||
If you're able to use go modules, you can clone the project wherever you want to and work from there.
|
||||
|
||||
#### Development-setup without go modules
|
||||
|
||||
Some internal packages are referenced using their respective package URL. This can become problematic.
|
||||
To “trick” the Go tool into thinking this is a clone from the official repository, download the source code
|
||||
into `$GOPATH/code.vikunja.io/api`. Fork the Vikunja repository, it should then be possible to switch the source directory on the command line.
|
||||
|
||||
{{< highlight bash >}}
|
||||
cd $GOPATH/src/code.vikunja.io/api
|
||||
{{< /highlight >}}
|
||||
|
||||
To be able to create pull requests, the forked repository should be added as a remote to the Vikunja sources, otherwise changes can’t be pushed.
|
||||
|
||||
{{< highlight bash >}}
|
||||
git remote rename origin upstream
|
||||
git remote add origin git@git.kolaente.de:<USERNAME>/api.git
|
||||
git fetch --all --prune
|
||||
{{< /highlight >}}
|
||||
|
||||
This should provide a working development environment for Vikunja. Take a look at the Magefile to get an overview about
|
||||
the available tasks. The most common tasks should be `mage test:unit` which will start our test environment and `mage build:build`
|
||||
which will build a vikunja binary into the working directory. Writing test cases is not mandatory to contribute, but it
|
||||
is highly encouraged and helps developers sleep at night.
|
||||
|
||||
That’s it! You are ready to hack on Vikunja. Test changes, push them to the repository, and open a pull request.
|
||||
|
||||
## Static assets
|
||||
|
||||
Each Vikunja release contains all static assets directly compiled into the binary.
|
||||
To prevent this during development, use the `dev` tag when developing.
|
||||
|
||||
See the [mage docs](mage.md#statically-compile-all-templates-into-the-binary) about how to compile with static assets for a release.
|
||||
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" >}}).
|
||||
|
|
|
@ -5,7 +5,7 @@ draft: false
|
|||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "practical instructions"
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Custom Errors
|
|
@ -28,11 +28,11 @@ This document explains how events and listeners work in Vikunja, how to use them
|
|||
|
||||
Each event has to implement this interface:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
type Event interface {
|
||||
Name() string
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
An event can contain whatever data you need.
|
||||
|
||||
|
@ -75,7 +75,7 @@ To dispatch an event, simply call the `events.Dispatch` method and pass in the e
|
|||
|
||||
The `TaskCreatedEvent` is declared in the `pkg/models/events.go` file as follows:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
// TaskCreatedEvent represents an event where a task has been created
|
||||
type TaskCreatedEvent struct {
|
||||
Task *Task
|
||||
|
@ -86,11 +86,11 @@ type TaskCreatedEvent struct {
|
|||
func (t *TaskCreatedEvent) Name() string {
|
||||
return "task.created"
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
It is dispatched in the `createTask` function of the `models` package:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err error) {
|
||||
|
||||
// ...
|
||||
|
@ -102,7 +102,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
|
|||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
As you can see, the curent task and doer are injected into it.
|
||||
|
||||
|
@ -122,13 +122,13 @@ A single event can have multiple listeners who are independent of each other.
|
|||
|
||||
All listeners must implement this interface:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
// Listener represents something that listens to events
|
||||
type Listener interface {
|
||||
Handle(msg *message.Message) error
|
||||
Name() string
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
The `Handle` method is executed when the event this listener listens on is dispatched.
|
||||
* As the single parameter, it gets the payload of the event, which is the event struct when it was dispatched decoded as json object and passed as a slice of bytes.
|
||||
|
@ -165,7 +165,7 @@ See the example below.
|
|||
|
||||
### Example
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
// RegisterListeners registers all event listeners
|
||||
func RegisterListeners() {
|
||||
events.RegisterListener((&ListCreatedEvent{}).Name(), &IncreaseListCounter{})
|
||||
|
@ -183,13 +183,29 @@ func (s *IncreaseTaskCounter) Name() string {
|
|||
func (s *IncreaseTaskCounter) Handle(payload message.Payload) (err error) {
|
||||
return keyvalue.IncrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
## Testing
|
||||
|
||||
When testing, you should call the `events.Fake()` method in the `TestMain` function of the package you want to test.
|
||||
This prevents any events from being fired and lets you assert an event has been dispatched like so:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
events.AssertDispatched(t, &TaskCreatedEvent{})
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
### Testing a listener
|
||||
|
||||
You can call an event listener manually with the `events.TestListener` method like so:
|
||||
|
||||
{{< highlight golang >}}
|
||||
ev := &TaskCommentCreatedEvent{
|
||||
Task: &task,
|
||||
Doer: u,
|
||||
Comment: tc,
|
||||
}
|
||||
|
||||
events.TestListener(t, ev, &SendTaskCommentNotification{})
|
||||
{{< /highlight >}}
|
||||
|
||||
This will call the listener's `Handle` method and assert it did not return an error when calling.
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Add a new api endpoint"
|
||||
title: "New API Endpoints"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "practical instructions"
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Add a new api endpoint/feature
|
|
@ -39,7 +39,7 @@ There are multiple categories of subcommands in the magefile:
|
|||
|
||||
## CI
|
||||
|
||||
These tasks are automatically run in our CI every time someone pushes to master or you update a pull request:
|
||||
These tasks are automatically run in our CI every time someone pushes to main or you update a pull request:
|
||||
|
||||
* `mage check:lint`
|
||||
* `mage check:fmt`
|
||||
|
@ -57,15 +57,13 @@ These tasks are automatically run in our CI every time someone pushes to master
|
|||
mage build:build
|
||||
{{< /highlight >}}
|
||||
|
||||
Builds a `vikunja`-binary in the root directory of the repo for the platform it is run on.
|
||||
|
||||
### Statically compile all templates into the binary
|
||||
or
|
||||
|
||||
{{< highlight bash >}}
|
||||
mage build:generate
|
||||
mage build
|
||||
{{< /highlight >}}
|
||||
|
||||
This generates static code with all templates, meaning no template need to be referenced at runtime.
|
||||
Builds a `vikunja`-binary in the root directory of the repo for the platform it is run on.
|
||||
|
||||
### clean
|
||||
|
||||
|
@ -73,7 +71,7 @@ This generates static code with all templates, meaning no template need to be re
|
|||
mage build:clean
|
||||
{{< /highlight >}}
|
||||
|
||||
Cleans all build, executable and bindata files
|
||||
Cleans all build and executable files
|
||||
|
||||
## Check
|
||||
|
||||
|
@ -173,6 +171,8 @@ mage dev:create-migration
|
|||
Creates a new migration with the current date.
|
||||
Will ask for the name of the struct you want to create a migration for.
|
||||
|
||||
See also [migration docs]({{< ref "mage.md" >}}).
|
||||
|
||||
## Misc
|
||||
|
||||
### Format the code
|
||||
|
|
|
@ -5,7 +5,7 @@ draft: false
|
|||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "practical instructions"
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Metrics
|
||||
|
@ -17,7 +17,7 @@ The `metrics` package provides several functions to create and update metrics.
|
|||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## New metrics
|
||||
## Exposing New Metrics
|
||||
|
||||
First, define a `const` with the metric key in redis. This is done in `pkg/metrics/metrics.go`.
|
||||
|
|
@ -14,7 +14,17 @@ It is possible to migrate data from other to-do services to Vikunja.
|
|||
To make this easier, we have put together a few helpers which are documented on this page.
|
||||
|
||||
In general, each migrator implements a migrator interface which is then called from a client.
|
||||
The interface makes it possible to use helper methods which handle http an focus only on the implementation of the migrator itself.
|
||||
The interface makes it possible to use helper methods which handle http and focus only on the implementation of the migrator itself.
|
||||
|
||||
There are two ways of migrating data from another service:
|
||||
1. Through the auth-based flow where the user gives you access to their data at the third-party service through an
|
||||
oauth flow. You can then call the service's api on behalf of your user to get all the data.
|
||||
The Todoist, Trello and Microsoft To-Do Migrators use this pattern.
|
||||
2. A file migration where the user uploads a file obtained from some third-party service. In your migrator, you need
|
||||
to parse the file and create the lists, tasks etc.
|
||||
The Vikunja File Import uses this pattern.
|
||||
|
||||
To differentiate the two, there are two different interfaces you must implement.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
|
@ -23,13 +33,16 @@ The interface makes it possible to use helper methods which handle http an focus
|
|||
All migrator implementations live in their own package in `pkg/modules/migration/<name-of-the-service>`.
|
||||
When creating a new migrator, you should place all related code inside that module.
|
||||
|
||||
## Migrator interface
|
||||
## Migrator Interface
|
||||
|
||||
The migrator interface is defined as follows:
|
||||
|
||||
```go
|
||||
// Migrator is the basic migrator interface which is shared among all migrators
|
||||
type Migrator interface {
|
||||
// Name holds the name of the migration.
|
||||
// This is used to show the name to users and to keep track of users who already migrated.
|
||||
Name() string
|
||||
// Migrate is the interface used to migrate a user's tasks from another platform to vikunja.
|
||||
// The user object is the user who's tasks will be migrated.
|
||||
Migrate(user *models.User) error
|
||||
|
@ -37,9 +50,20 @@ type Migrator interface {
|
|||
// The use case for this are Oauth flows, where the server token should remain hidden and not
|
||||
// known to the frontend.
|
||||
AuthURL() string
|
||||
}
|
||||
```
|
||||
|
||||
## File Migrator Interface
|
||||
|
||||
```go
|
||||
// FileMigrator handles importing Vikunja data from a file. The implementation of it determines the format.
|
||||
type FileMigrator interface {
|
||||
// Name holds the name of the migration.
|
||||
// This is used to show the name to users and to keep track of users who already migrated.
|
||||
Name() string
|
||||
// Migrate is the interface used to migrate a user's tasks, list and other things from a file to vikunja.
|
||||
// The user object is the user who's tasks will be migrated.
|
||||
Migrate(user *user.User, file io.ReaderAt, size int64) error
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -54,23 +78,35 @@ authUrl, Status and Migrate methods.
|
|||
```go
|
||||
// This is an example for the Wunderlist migrator
|
||||
if config.MigrationWunderlistEnable.GetBool() {
|
||||
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
|
||||
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
|
||||
MigrationStruct: func() migration.Migrator {
|
||||
return &wunderlist.Migration{}
|
||||
},
|
||||
}
|
||||
wunderlistMigrationHandler.RegisterRoutes(m)
|
||||
wunderlistMigrationHandler.RegisterRoutes(m)
|
||||
}
|
||||
```
|
||||
|
||||
You should also document the routes with [swagger annotations]({{< ref "../practical-instructions/swagger-docs.md" >}}).
|
||||
And for the file migrator:
|
||||
|
||||
```go
|
||||
vikunjaFileMigrationHandler := &migrationHandler.FileMigratorWeb{
|
||||
MigrationStruct: func() migration.FileMigrator {
|
||||
return &vikunja_file.FileMigrator{}
|
||||
},
|
||||
}
|
||||
vikunjaFileMigrationHandler.RegisterRoutes(m)
|
||||
```
|
||||
|
||||
You should also document the routes with [swagger annotations]({{< ref "swagger-docs.md" >}}).
|
||||
|
||||
## Insertion helper method
|
||||
|
||||
There is a method available in the `migration` package which takes a fully nested Vikunja structure and creates it with all relations.
|
||||
This means you start by adding a namespace, then add lists inside of that namespace, then tasks in the lists and so on.
|
||||
|
||||
The root structure must be present as `[]*models.NamespaceWithLists`.
|
||||
The root structure must be present as `[]*models.NamespaceWithListsAndTasks`. It allows to represent all of Vikunja's
|
||||
hierachie as a single data structure.
|
||||
|
||||
Then call the method like so:
|
||||
|
||||
|
@ -85,14 +121,16 @@ err = migration.InsertFromStructure(fullVikunjaHierachie, user)
|
|||
|
||||
## Configuration
|
||||
|
||||
You should add at least an option to enable or disable the migration.
|
||||
If your migrator is an oauth-based one, you should add at least an option to enable or disable it.
|
||||
Chances are, you'll need some more options for things like client ID and secret
|
||||
(if the other service uses oAuth as an authentication flow).
|
||||
|
||||
The easiest way to implement an on/off switch is to check whether your migration service is enabled or not when
|
||||
registering the routes, and then simply don't registering the routes in the case it is disabled.
|
||||
registering the routes, and then simply don't registering the routes in case it is disabled.
|
||||
|
||||
File based migrators can always be enabled.
|
||||
|
||||
### Making the migrator public in `/info`
|
||||
|
||||
You should make your migrator available in the `/info` endpoint so that frontends can display options to enable them or not.
|
||||
To do this, add an entry to `pkg/routes/api/v1/info.go`.
|
||||
To do this, add an entry to the `AvailableMigrators` field in `pkg/routes/api/v1/info.go`.
|
||||
|
|
|
@ -18,13 +18,13 @@ Vikunjs provides a simple abstraction to send notifications per mail and in the
|
|||
|
||||
Each notification has to implement this interface:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
type Notification interface {
|
||||
ToMail() *Mail
|
||||
ToDB() interface{}
|
||||
Name() string
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
Both functions return the formatted messages for mail and database.
|
||||
|
||||
|
@ -35,7 +35,7 @@ For example, if your notification should not be recorded in the database but onl
|
|||
|
||||
A list of chainable functions is available to compose a mail:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
mail := NewMail().
|
||||
// The optional sender of the mail message.
|
||||
From("test@example.com").
|
||||
|
@ -54,7 +54,7 @@ mail := NewMail().
|
|||
Action("The Action", "https://example.com").
|
||||
// Another line of text.
|
||||
Line("This should be an outro line").
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
If not provided, the `from` field of the mail contains the value configured in [`mailer.fromemail`](https://vikunja.io/docs/config-options/#fromemail).
|
||||
|
||||
|
@ -75,14 +75,14 @@ It takes the name of the notification and the package where the notification wil
|
|||
Notifiables can receive a notification.
|
||||
A notifiable is defined with this interface:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
type Notifiable interface {
|
||||
// Should return the email address this notifiable has.
|
||||
RouteForMail() string
|
||||
// Should return the id of the notifiable entity
|
||||
RouteForDB() int64
|
||||
}
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
The `User` type from the `user` package implements this interface.
|
||||
|
||||
|
@ -93,7 +93,7 @@ It takes a notifiable and a notification as input.
|
|||
|
||||
For example, the email confirm notification when a new user registers is sent like this:
|
||||
|
||||
```golang
|
||||
{{< highlight golang >}}
|
||||
n := &EmailConfirmNotification{
|
||||
User: update.User,
|
||||
IsNew: false,
|
||||
|
@ -101,13 +101,20 @@ n := &EmailConfirmNotification{
|
|||
|
||||
err = notifications.Notify(update.User, n)
|
||||
return
|
||||
```
|
||||
{{< /highlight >}}
|
||||
|
||||
## Testing
|
||||
|
||||
The `mail` package provides a `Fake()` method which you should call in the `MainTest` functions of your package.
|
||||
If it was called, no mails are being sent and you can instead assert they have been sent with the `AssertSent` method.
|
||||
|
||||
When testing, you should call the `notifications.Fake()` method in the `TestMain` function of the package you want to test.
|
||||
This prevents any notifications from being sent and lets you assert a notifications has been sent like this:
|
||||
|
||||
{{< highlight golang >}}
|
||||
notifications.AssertSent(t, &ReminderDueNotification{})
|
||||
{{< /highlight >}}
|
||||
|
||||
## Example
|
||||
|
||||
Take a look at the [pkg/user/notifications.go](https://code.vikunja.io/api/src/branch/master/pkg/user/notifications.go) file for a good example.
|
||||
Take a look at the [pkg/user/notifications.go](https://code.vikunja.io/api/src/branch/main/pkg/user/notifications.go) file for a good example.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Project structure"
|
||||
title: "Project Structure"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
|
@ -10,40 +10,7 @@ menu:
|
|||
|
||||
# Project structure
|
||||
|
||||
In general, this api repo has the following structure:
|
||||
|
||||
* `docker`
|
||||
* `docs`
|
||||
* `pkg`
|
||||
* `caldav`
|
||||
* `cmd`
|
||||
* `config`
|
||||
* `db`
|
||||
* `fixtures`
|
||||
* `files`
|
||||
* `integration`
|
||||
* `log`
|
||||
* `mail`
|
||||
* `metrics`
|
||||
* `migration`
|
||||
* `models`
|
||||
* `modules`
|
||||
* `migration`
|
||||
* `handler`
|
||||
* `wunderlist`
|
||||
* `red`
|
||||
* `routes`
|
||||
* `api/v1`
|
||||
* `static`
|
||||
* `swagger`
|
||||
* `user`
|
||||
* `utils`
|
||||
* `version`
|
||||
* `REST-Tests`
|
||||
* `templates`
|
||||
* `vendor`
|
||||
|
||||
This document will explain what these mean and what you can find where.
|
||||
This document explains what each package does.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
|
@ -52,18 +19,13 @@ This document will explain what these mean and what you can find where.
|
|||
The root directory is where [the config file]({{< ref "../setup/config.md">}}), [Magefile]({{< ref "mage.md">}}), license, drone config,
|
||||
application entry point (`main.go`) and so on are located.
|
||||
|
||||
## docker
|
||||
|
||||
This directory holds additonal files needed to build and run the docker container, mainly service configuration to properly run Vikunja inside a docker
|
||||
container.
|
||||
|
||||
## pkg
|
||||
|
||||
This is where most of the magic happens. Most packages with actual code are located in this folder.
|
||||
|
||||
### caldav
|
||||
|
||||
This folder holds a simple caldav implementation which is responsible for returning the caldav feature.
|
||||
This folder holds a simple caldav implementation which is responsible for the caldav feature.
|
||||
|
||||
### cmd
|
||||
|
||||
|
@ -75,10 +37,15 @@ To learn more about how to use this cli, see [the cli usage docs]({{< ref "../us
|
|||
|
||||
### config
|
||||
|
||||
This package configures the config. It sets default values and sets up viper and tells it where to look for config files,
|
||||
how to interpret which env variables for config etc.
|
||||
This package configures handling of Vikunja's runtime configuration.
|
||||
It sets default values and sets up viper and tells it where to look for config files, how to interpret which env variables
|
||||
for config etc.
|
||||
|
||||
If you want to add a new config parameter, you should add default value in this package.
|
||||
See also the [docs about adding a new configuration parameter]({{< ref "config.md" >}}).
|
||||
|
||||
### cron
|
||||
|
||||
See [how to add a cron task]({{< ref "cron.md" >}}).
|
||||
|
||||
### db
|
||||
|
||||
|
@ -102,17 +69,17 @@ This init is called in `main.go` after the config init is done.
|
|||
|
||||
### mail
|
||||
|
||||
This package handles all mail sending. To learn how to send a mail, see [sending emails]({{< ref "../practical-instructions/mail.md">}}).
|
||||
This package handles all mail sending. To learn how to send a mail, see [notifications]({{< ref "notifications.md" >}}).
|
||||
|
||||
### metrics
|
||||
|
||||
This package handles all metrics which are exposed to the prometheus endpoint.
|
||||
To learn how it works and how to add new metrics, take a look at [how metrics work]({{< ref "../practical-instructions/metrics.md">}}).
|
||||
To learn how it works and how to add new metrics, take a look at [how metrics work]({{< ref "metrics.md">}}).
|
||||
|
||||
### migration
|
||||
|
||||
This package handles all migrations.
|
||||
All migrations are stored and executed here.
|
||||
All migrations are stored and executed in this package.
|
||||
|
||||
To learn more, take a look at the [migrations docs]({{< ref "../development/db-migrations.md">}}).
|
||||
|
||||
|
@ -123,11 +90,35 @@ When adding new features or upgrading existing ones, that most likely happens he
|
|||
|
||||
Because this package is pretty huge, there are several documents and how-to's about it:
|
||||
|
||||
* [Adding a feature]({{< ref "../practical-instructions/feature.md">}})
|
||||
* [Making calls to the database]({{< ref "../practical-instructions/database.md">}})
|
||||
* [Adding a feature]({{< ref "feature.md">}})
|
||||
* [Making calls to the database]({{< ref "database.md">}})
|
||||
|
||||
### modules
|
||||
|
||||
Everything that can have multiple implementations (like a task migrator from a third-party task provider) lives in a
|
||||
respective sub package in this package.
|
||||
|
||||
#### auth
|
||||
|
||||
Contains openid related authentication.
|
||||
|
||||
#### avatar
|
||||
|
||||
Contains all possible avatar providers a user can choose to set their avatar.
|
||||
|
||||
#### background
|
||||
|
||||
All list background providers are in sub-packages of this package.
|
||||
|
||||
#### dump
|
||||
|
||||
Handles everything related to the `dump` and `restore` commands of Vikunja.
|
||||
|
||||
#### keyvalue
|
||||
|
||||
A simple key-value store with an implementation for memory and redis.
|
||||
Can be used to cache values.
|
||||
|
||||
#### migration
|
||||
|
||||
See [writing a migrator]({{< ref "migration.md" >}}).
|
||||
|
@ -142,20 +133,19 @@ to talk to redis.
|
|||
|
||||
It uses the [go-redis](https://github.com/go-redis/redis) library, please see their configuration on how to use it.
|
||||
|
||||
**Note**: Only use this package directly if you have to use a direct redis connection.
|
||||
In most cases, using the `keyvalue` package is a better fit.
|
||||
|
||||
### routes
|
||||
|
||||
This package defines all routes which are available for vikunja clients to use.
|
||||
To add a new route, see [adding a new route]({{< ref "../practical-instructions/feature.md">}}).
|
||||
To add a new route, see [adding a new route]({{< ref "feature.md">}}).
|
||||
|
||||
#### api/v1
|
||||
|
||||
This is where all http-handler functions for the api are stored.
|
||||
Every handler function which does not use the standard web handler should live here.
|
||||
|
||||
### static
|
||||
|
||||
All static files generated by `mage generate` live here.
|
||||
|
||||
### swagger
|
||||
|
||||
This is where the [generated]({{< ref "mage.md#generate-swagger-definitions-from-code-comments">}} [api docs]({{< ref "../usage/api.md">}}) live.
|
||||
|
@ -179,23 +169,3 @@ See their function definitions for instructions on how to use them.
|
|||
The single purpouse of this package is to hold the current vikunja version which gets overridden through build flags
|
||||
each time `mage release` or `mage build` is run.
|
||||
It is a seperate package to avoid import cycles with other packages.
|
||||
|
||||
## REST-Tests
|
||||
|
||||
Holds all kinds of test files to directly test the api from inside of [jetbrains ide's](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html).
|
||||
|
||||
These files are currently more an experiment, maybe we will drop them in the future to use something we could integrate in the testing process with drone.
|
||||
Therefore, this has no claim to be complete yet even working, you're free to change whatever is needed to get it working for you.
|
||||
|
||||
## templates
|
||||
|
||||
Holds the email templates used to send plain text and html emails for new user registration and password changes.
|
||||
|
||||
## vendor
|
||||
|
||||
All libraries needed to build Vikunja.
|
||||
|
||||
We keep all libraries used for Vikunja around in the `vendor/` folder to still be able to build the project even if
|
||||
some maintainers take their libraries down like [it happened in the past](https://github.com/jteeuwen/go-bindata/issues/5).
|
||||
|
||||
When adding a new dependency, make sure to run `go mod vendor` to put it inside this directory.
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Modifying swagger api docs"
|
||||
title: "Modifying Swagger API Docs"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "practical instructions"
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Adding/editing swagger api docs
|
||||
# Modifying swagger api docs
|
||||
|
||||
The api documentation is generated using [swaggo](https://github.com/swaggo/swag) from comments.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Documenting structs
|
||||
|
||||
You should always comment every field which will be exposed as a json in the api.
|
||||
|
@ -45,3 +47,27 @@ type List struct {
|
|||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
## Documenting api Endpoints
|
||||
|
||||
All api routes should be documented with a comment above the handler function.
|
||||
When generating the api docs with mage, the swagger cli will pick these up and put them in a neat document.
|
||||
|
||||
A comment looks like this:
|
||||
|
||||
{{< highlight golang >}}
|
||||
// @Summary Login
|
||||
// @Description Logs a user in. Returns a JWT-Token to authenticate further requests.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param credentials body user.Login true "The login credentials"
|
||||
// @Success 200 {object} auth.Token
|
||||
// @Failure 400 {object} models.Message "Invalid user password model."
|
||||
// @Failure 412 {object} models.Message "Invalid totp passcode."
|
||||
// @Failure 403 {object} models.Message "Invalid username or password."
|
||||
// @Router /login [post]
|
||||
func Login(c echo.Context) error {
|
||||
// Handler logic
|
||||
}
|
||||
{{< /highlight >}}
|
|
@ -10,7 +10,7 @@ menu:
|
|||
|
||||
# Testing
|
||||
|
||||
You can run unit tests with [our `Magefile`]({{< ref "mage.md">}}) with
|
||||
You can run unit tests with [mage]({{< ref "mage.md">}}) with
|
||||
|
||||
{{< highlight bash >}}
|
||||
mage test:unit
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
title: "German Translation Instructions"
|
||||
date: 2021-06-23T23:47:34+02:00
|
||||
draft: false
|
||||
---
|
||||
|
||||
# German Translation Instructions
|
||||
|
||||
<div class="notification is-warning">
|
||||
<b>NOTE:</b> This document contains translation instructions specific to the german translation of Vikunja.
|
||||
For instructions applicable to all languages, check out the <a href="{{< ref "./translations.md">}}">general translation instructions</a>.
|
||||
</div>
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Allgemein
|
||||
|
||||
Anrede: Wenig förmlich:
|
||||
|
||||
* “Du”-Form
|
||||
* Keine “Amtsdeusch“-Umschreibungen, einfach so als ob man den Nutzer direkt persönlich ansprechen würde
|
||||
|
||||
Genauer definiert:
|
||||
|
||||
* “falsch” anstatt “nicht korrekt/inkorrekt”
|
||||
* “Wende dich an …” anstatt “kontaktiere …”
|
||||
* In derselben Zeit übersetzen (sonst wird aus dem englischen “is“ das deutsche “war”)
|
||||
* Richtige Anführungszeichen verwenden. Also `„“` statt `''` oder `'` oder ` oder ´
|
||||
* `„` für beginnende Anführungszeichen, `“` für schließende Anführungszeichen
|
||||
|
||||
Es gelten Artikel und Worttrennungen aus dem [Duden](https://duden.de).
|
||||
|
||||
## Formulierungen
|
||||
|
||||
* `Account` statt `Konto`.
|
||||
* `TOTP` immer als ein Wort und Groß.
|
||||
* `CalDAV` immer so.
|
||||
* `löschen` oder `entfernen` je nach Kontext. Wenn etwas *gelöscht* wird, existiert das gelöschte Objekt und danach
|
||||
nicht mehr und hat evtl. andere Objekte mitgelöscht (z.B. eine Aufgabe). Wird etwas *entfernt*, bezieht sich das
|
||||
meistens auf die Beziehung zu einem anderen Objekt. Das entfernte Objekt existiert danach immernoch, z.B. beim
|
||||
Entfernen eine:r Nutzer:in aus einem Team.
|
||||
* Analog zu `löschen` oder `entfernen` gilt ähnliches für `hinzufügen` oder `erstellen`. Eine Aufgabe wird *erstellt*,
|
||||
aber ein:e Nutzer:in nur zu einem Team *hinzugefügt*.
|
||||
* `Anmeldename` anstatt `Benutzer:innenname`
|
||||
|
||||
## Formulierungen in Modals und Buttons
|
||||
|
||||
Es sollten die gleichen Formulierungen auf Buttons und Modals verwendet werden.
|
||||
|
||||
Beispiel: Wenn der Button mit `löschen` beschriftet ist, sollte im Modal die Frage
|
||||
lauten `Willst du das wirklich löschen?` und nicht `Willst du das wirklich entfernen?`. Gleiches gilt für
|
||||
Erfolgs/Fehlermeldungen nach der Aktion.
|
||||
|
||||
## Gendern
|
||||
|
||||
Wo möglich, sollte eine geschlechtsneutrale Anrede verwendet werden. Falls diese sehr umständlich würden (siehe oben
|
||||
„Amtsdeutsch-Umschreibungen“), soll mit *Doppelpunkt* gegendert werden.
|
||||
|
||||
Beispiel: „Benutzer:in“
|
||||
|
||||
## Trennungen
|
||||
|
||||
* E-Mail-Adresse (siehe Duden)
|
||||
|
||||
## Wörter und Ausdrücke
|
||||
|
||||
| Englisches Original | Verwendung in deutscher Übersetzung |
|
||||
| ------------------- | -------------------- |
|
||||
| Bucket | Spalte |
|
||||
| Namespace | Namespace |
|
||||
| Link Share | Linkfreigabe |
|
||||
| Username | Anmeldename |
|
||||
|
||||
## Weiterführende Links
|
||||
|
||||
* http://docs.translatehouse.org/projects/localization-guide/en/latest/guide/translation_guidelines_german.html
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
title: "Translations"
|
||||
date: 2021-06-23T22:52:06+02:00
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "development"
|
||||
---
|
||||
|
||||
# Translations
|
||||
|
||||
This document provides documentation about how to translate Vikunja.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Where to translate
|
||||
|
||||
Translation happens at [crowdin](https://crowdin.com/project/vikunja).
|
||||
|
||||
Currently, only the frontend (and by extension, the desktop app) is translatable.
|
||||
|
||||
## Translation Instructions
|
||||
|
||||
> These are the instructions for translating Vikunja in another language.
|
||||
> For information about how to add new translation strings, see below.
|
||||
|
||||
For all languages these translation guidelines should be applied when translating:
|
||||
|
||||
* Use a less-formal style, as if you were talking to a friend.
|
||||
* If the source string contains characters like `&` or `…`, the translated string should contain them as well.
|
||||
|
||||
More specific instructions for some languages can be found below.
|
||||
|
||||
### Wrong translation strings
|
||||
|
||||
If you encounter a wrong original translation string while translating, please don't correct it in the translation.
|
||||
Instead, translate it to reflect the original meaning in the translated string but add a comment under the source string to discuss potential changes.
|
||||
|
||||
### Language-specific instructions
|
||||
|
||||
* [German]({{< ref "./translation-instructions-german.md">}})
|
||||
|
||||
## How to add new translation strings
|
||||
|
||||
All translation strings are stored in `src/i18n/lang/`.
|
||||
New strings should be added only in the `en.json` file.
|
||||
Strings in other languages will be synced through weblate and should not be added directly as a PR/commit in the frontend repo.
|
||||
|
||||
## Requesting a new language
|
||||
|
||||
If you want to start translating Vikunja in a language not yet available in Vikunja, please request the language through the crowdin interface.
|
||||
If you have issues with this or need a discussion before doing so, please [contact us](https://vikunja.io/contact/) or [start a discussion in the forum](https://community.vikunja.io).
|
||||
|
||||
Once at least 50% of all translation strings are translated and approved, they will be added and distributed with the Vikunja frontend for users to select and use Vikunja with them.
|
|
@ -1,40 +0,0 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Database"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "practical instructions"
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
Inside the `models` package, a variable `x` is available which contains a pointer to an instance of `xorm.Engine`.
|
||||
This is used whenever you make a call to the database to get or update data.
|
||||
|
||||
This xorm instance is set up and initialized every time vikunja is started.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Adding new database tables
|
||||
|
||||
To add a new table to the database, add a an instance of your struct to the `tables` variable in the
|
||||
init function in `pkg/models/models.go`. Xorm will sync them automatically.
|
||||
|
||||
You also need to add a pointer to the `tablesWithPointer` slice to enable caching for all instances of this struct.
|
||||
|
||||
To learn more about how to configure your struct to create "good" tables, refer to [the xorm documentaion](http://xorm.io/docs/).
|
||||
|
||||
## Adding data to test fixtures
|
||||
|
||||
Adding data for test fixtures is done in via `yaml` files insinde of `pkg/models/fixtures`.
|
||||
|
||||
The name of the yaml file should equal the table name in the database.
|
||||
Adding values to it is done via array definition inside of the yaml file.
|
||||
|
||||
**Note**: Table and column names need to be in snake_case as that's what is used internally in the database
|
||||
and for mapping values from the database to xorm so your structs can use it.
|
|
@ -1,86 +0,0 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Mailer"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "practical instructions"
|
||||
---
|
||||
|
||||
# Mailer
|
||||
|
||||
This document explains how to use the mailer to send emails and what to do to create a new kind of email to be sent.
|
||||
|
||||
{{< table_of_contents >}}
|
||||
|
||||
## Sending emails
|
||||
|
||||
**Note:** You should use mail templates whenever possible (see below).
|
||||
|
||||
To send an email, use the function `mail.SendMail(options)`. The options are defined as follows:
|
||||
|
||||
{{< highlight golang >}}
|
||||
type Opts struct {
|
||||
To string // The email address of the recipent
|
||||
Subject string // The subject of the mail
|
||||
Message string // The plaintext message in the mail
|
||||
HTMLMessage string // The html message
|
||||
ContentType ContentType // The content type of the mail. Can be either mail.ContentTypePlain, mail.ContentTypeHTML, mail.ContentTypeMultipart. You should set this according to the kind of mail you want to send.
|
||||
Boundary string
|
||||
Headers []*header // Other headers to set in the mail.
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
### Sending emails based on a template
|
||||
|
||||
For each mail with a template, there are two email templates: One for plaintext emails, one for html emails.
|
||||
|
||||
These are located in the `templates/mail` folder and follow the conventions of `template-name.{plain|hmtl}.tmpl`,
|
||||
both the plaintext and html templates are in the same folder.
|
||||
|
||||
To send a mail based on a template, use the function `mail.SendMailWithTemplate(to, subject, tpl string, data map[string]interface{})`.
|
||||
`to` and `subject` are pretty much self-explanatory, `tpl` is the name of the template, without `.html.tmpl` or `.plain.tmpl`.
|
||||
`data` is a map you can pass additional data to your template.
|
||||
|
||||
### Sending a mail with a template
|
||||
|
||||
A basic html email template would look like this:
|
||||
|
||||
{{< highlight go-html-template >}}
|
||||
{{template "mail-header.tmpl" .}}
|
||||
<p>
|
||||
Hey there!<br/>
|
||||
This is a minimal html email example.<br/>
|
||||
{{.Something}}
|
||||
</p>
|
||||
{{template "mail-footer.tmpl"}}
|
||||
{{< /highlight >}}
|
||||
|
||||
And the corresponding plaintext template:
|
||||
|
||||
{{< highlight go-text-template >}}
|
||||
Hey there!
|
||||
|
||||
This is a minimal html email example.
|
||||
|
||||
{{.Something}}
|
||||
{{< /highlight >}}
|
||||
You would then call this like so:
|
||||
|
||||
{{< highlight golang >}}
|
||||
data := make(map[string]interface{})
|
||||
data["Something"] = "I am some computed value"
|
||||
to := "test@example.com"
|
||||
subject := "A simple test mail"
|
||||
tpl := "demo" // Assuming you saved the templates as demo.plain.tmpl and demo.html.tmpl
|
||||
mail.SendMailWithTemplate(to, subject, tpl, data)
|
||||
{{< /highlight >}}
|
||||
|
||||
The function does not return an error. If an error occures when sending a mail, it is logged but not returned because sending the mail happens asinchrounly.
|
||||
|
||||
Notice the `mail-header.tmpl` and `mail-footer.tmpl` in the template. These populate some basic css, a box for your content and the vikunja logo.
|
||||
All that's left for you is to put the content in, which then will appear in a beautifully-styled box.
|
||||
|
||||
Remeber, these are email templates. This is different from normal html/css, you cannot use whatever you want (because most of the clients are wayyy to outdated).
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
---
|
||||
date: "2019-02-12:00:00+02:00"
|
||||
title: "Adding new config options"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "practical instructions"
|
||||
---
|
||||
|
||||
# Adding new config options
|
||||
|
||||
Vikunja uses [viper](https://github.com/spf13/viper) to handle configuration options.
|
||||
It handles parsing all different configuration sources.
|
||||
|
||||
The configuration is done in sections. These are represented with a `.` in viper.
|
||||
Take a look at `pkg/config/config.go` to understand how these are set.
|
||||
|
||||
To add a new config option, you should add a default value to `pkg/config/config.go`.
|
||||
Default values should always enable the feature to work somehow, or turn it off completely if it always needs
|
||||
additional configuration.
|
||||
|
||||
Make sure to add the new config option to [the config document]({{< ref "../setup/config.md">}}) and the default config file
|
||||
(`config.yml.sample` at the root of the repository) to make sure it is well documented.
|
||||
|
||||
If you're using a computed value as a default, make sure to update the sample config file and debian
|
||||
post-install scripts to reflect that.
|
||||
|
||||
To get a configured option, use `viper.Get("config.option")`.
|
||||
Take a look at [viper's documentation](https://github.com/spf13/viper#getting-values-from-viper) to learn of the
|
||||
different ways available to get config options.
|
|
@ -32,7 +32,7 @@ first:
|
|||
Vikunja supports using `toml`, `yaml`, `hcl`, `ini`, `json`, envfile, env variables and Java Properties files.
|
||||
We reccomend yaml or toml, but you're free to use whatever you want.
|
||||
|
||||
Vikunja provides a default [`config.yml`](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) file which you can use as a starting point.
|
||||
Vikunja provides a default [`config.yml`](https://kolaente.dev/vikunja/api/src/branch/main/config.yml.sample) file which you can use as a starting point.
|
||||
|
||||
# Config file locations
|
||||
|
||||
|
@ -46,7 +46,7 @@ Vikunja will search on various places for a config file:
|
|||
# Default configuration with explanations
|
||||
|
||||
The following explains all possible config variables and their defaults.
|
||||
You can find a full example configuration file in [here](https://code.vikunja.io/api/src/branch/master/config.yml.sample).
|
||||
You can find a full example configuration file in [here](https://code.vikunja.io/api/src/branch/main/config.yml.sample).
|
||||
|
||||
If you don't provide a value in your config file, their default will be used.
|
||||
|
||||
|
@ -55,7 +55,7 @@ If you don't provide a value in your config file, their default will be used.
|
|||
Most config variables are nested under some "higher-level" key.
|
||||
For example, the `interface` config variable is a child of the `service` key.
|
||||
|
||||
The docs below aim to reflect that leveling, but please also have a lookt at [the default config](https://code.vikunja.io/api/src/branch/master/config.yml.sample) file
|
||||
The docs below aim to reflect that leveling, but please also have a lookt at [the default config](https://code.vikunja.io/api/src/branch/main/config.yml.sample) file
|
||||
to better grasp how the nesting looks like.
|
||||
|
||||
<!-- Generated config will be injected here -->
|
||||
|
@ -74,18 +74,55 @@ Default is a random token which will be generated at each startup of vikunja.
|
|||
|
||||
Default: `<jwt-secret>`
|
||||
|
||||
Full path: `service.JWTSecret`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_JWT_SECRET`
|
||||
|
||||
|
||||
### interface
|
||||
|
||||
The interface on which to run the webserver
|
||||
|
||||
Default: `:3456`
|
||||
|
||||
Full path: `service.interface`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_INTERFACE`
|
||||
|
||||
|
||||
### unixsocket
|
||||
|
||||
Path to Unix socket. If set, it will be created and used instead of tcp
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `service.unixsocket`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_UNIXSOCKET`
|
||||
|
||||
|
||||
### unixsocketmode
|
||||
|
||||
Permission bits for the Unix socket. Note that octal values must be prefixed by "0o", e.g. 0o660
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `service.unixsocketmode`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_UNIXSOCKETMODE`
|
||||
|
||||
|
||||
### frontendurl
|
||||
|
||||
The URL of the frontend, used to send password reset emails.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `service.frontendurl`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_FRONTENDURL`
|
||||
|
||||
|
||||
### rootpath
|
||||
|
||||
The base path on the file system where the binary and assets are.
|
||||
|
@ -94,66 +131,121 @@ with a config file which will then be used.
|
|||
|
||||
Default: `<rootpath>`
|
||||
|
||||
Full path: `service.rootpath`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ROOTPATH`
|
||||
|
||||
|
||||
### maxitemsperpage
|
||||
|
||||
The max number of items which can be returned per page
|
||||
|
||||
Default: `50`
|
||||
|
||||
Full path: `service.maxitemsperpage`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_MAXITEMSPERPAGE`
|
||||
|
||||
|
||||
### enablecaldav
|
||||
|
||||
Enable the caldav endpoint, see the docs for more details
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enablecaldav`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLECALDAV`
|
||||
|
||||
|
||||
### motd
|
||||
|
||||
Set the motd message, available from the /info endpoint
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `service.motd`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_MOTD`
|
||||
|
||||
|
||||
### enablelinksharing
|
||||
|
||||
Enable sharing of lists via a link
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enablelinksharing`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLELINKSHARING`
|
||||
|
||||
|
||||
### enableregistration
|
||||
|
||||
Whether to let new users registering themselves or not
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enableregistration`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLEREGISTRATION`
|
||||
|
||||
|
||||
### enabletaskattachments
|
||||
|
||||
Whether to enable task attachments or not
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enabletaskattachments`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLETASKATTACHMENTS`
|
||||
|
||||
|
||||
### timezone
|
||||
|
||||
The time zone all timestamps are in. Please note that time zones have to use [the official tz database names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). UTC or GMT offsets won't work.
|
||||
|
||||
Default: `GMT`
|
||||
|
||||
Full path: `service.timezone`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_TIMEZONE`
|
||||
|
||||
|
||||
### enabletaskcomments
|
||||
|
||||
Whether task comments should be enabled or not
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enabletaskcomments`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLETASKCOMMENTS`
|
||||
|
||||
|
||||
### enabletotp
|
||||
|
||||
Whether totp is enabled. In most cases you want to leave that enabled.
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enabletotp`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLETOTP`
|
||||
|
||||
|
||||
### sentrydsn
|
||||
|
||||
If not empty, enables logging of crashes and unhandled errors in sentry.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `service.sentrydsn`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_SENTRYDSN`
|
||||
|
||||
|
||||
### testingtoken
|
||||
|
||||
If not empty, this will enable `/test/{table}` endpoints which allow to put any content in the database.
|
||||
|
@ -163,6 +255,11 @@ each request made to this endpoint neefs to provide an `Authorization: <token>`
|
|||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `service.testingtoken`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_TESTINGTOKEN`
|
||||
|
||||
|
||||
### enableemailreminders
|
||||
|
||||
If enabled, vikunja will send an email to everyone who is either assigned to a task or created it when a task reminder
|
||||
|
@ -170,6 +267,24 @@ is due.
|
|||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enableemailreminders`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLEEMAILREMINDERS`
|
||||
|
||||
|
||||
### enableuserdeletion
|
||||
|
||||
If true, will allow users to request the complete deletion of their account. When using external authentication methods
|
||||
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.
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `service.enableuserdeletion`
|
||||
|
||||
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## database
|
||||
|
@ -182,54 +297,99 @@ Database type to use. Supported types are mysql, postgres and sqlite.
|
|||
|
||||
Default: `sqlite`
|
||||
|
||||
Full path: `database.type`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_TYPE`
|
||||
|
||||
|
||||
### user
|
||||
|
||||
Database user which is used to connect to the database.
|
||||
|
||||
Default: `vikunja`
|
||||
|
||||
Full path: `database.user`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_USER`
|
||||
|
||||
|
||||
### password
|
||||
|
||||
Databse password
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `database.password`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_PASSWORD`
|
||||
|
||||
|
||||
### host
|
||||
|
||||
Databse host
|
||||
|
||||
Default: `localhost`
|
||||
|
||||
Full path: `database.host`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_HOST`
|
||||
|
||||
|
||||
### database
|
||||
|
||||
Databse to use
|
||||
|
||||
Default: `vikunja`
|
||||
|
||||
Full path: `database.database`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_DATABASE`
|
||||
|
||||
|
||||
### path
|
||||
|
||||
When using sqlite, this is the path where to store the data
|
||||
|
||||
Default: `./vikunja.db`
|
||||
|
||||
Full path: `database.path`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_PATH`
|
||||
|
||||
|
||||
### maxopenconnections
|
||||
|
||||
Sets the max open connections to the database. Only used when using mysql and postgres.
|
||||
|
||||
Default: `100`
|
||||
|
||||
Full path: `database.maxopenconnections`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_MAXOPENCONNECTIONS`
|
||||
|
||||
|
||||
### maxidleconnections
|
||||
|
||||
Sets the maximum number of idle connections to the db.
|
||||
|
||||
Default: `50`
|
||||
|
||||
Full path: `database.maxidleconnections`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_MAXIDLECONNECTIONS`
|
||||
|
||||
|
||||
### maxconnectionlifetime
|
||||
|
||||
The maximum lifetime of a single db connection in miliseconds.
|
||||
|
||||
Default: `10000`
|
||||
|
||||
Full path: `database.maxconnectionlifetime`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_MAXCONNECTIONLIFETIME`
|
||||
|
||||
|
||||
### sslmode
|
||||
|
||||
Secure connection mode. Only used with postgres.
|
||||
|
@ -237,12 +397,22 @@ Secure connection mode. Only used with postgres.
|
|||
|
||||
Default: `disable`
|
||||
|
||||
Full path: `database.sslmode`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_SSLMODE`
|
||||
|
||||
|
||||
### tls
|
||||
|
||||
Enable SSL/TLS for mysql connections. Options: false, true, skip-verify, preferred
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `database.tls`
|
||||
|
||||
Environment path: `VIKUNJA_DATABASE_TLS`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## cache
|
||||
|
@ -255,6 +425,11 @@ If cache is enabled or not
|
|||
|
||||
Default: `false`
|
||||
|
||||
Full path: `cache.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_CACHE_ENABLED`
|
||||
|
||||
|
||||
### type
|
||||
|
||||
Cache type. Possible values are "keyvalue", "memory" or "redis".
|
||||
|
@ -263,12 +438,22 @@ When choosing "redis" you will need to configure the redis connection seperately
|
|||
|
||||
Default: `keyvalue`
|
||||
|
||||
Full path: `cache.type`
|
||||
|
||||
Environment path: `VIKUNJA_CACHE_TYPE`
|
||||
|
||||
|
||||
### maxelementsize
|
||||
|
||||
When using memory this defines the maximum size an element can take
|
||||
|
||||
Default: `1000`
|
||||
|
||||
Full path: `cache.maxelementsize`
|
||||
|
||||
Environment path: `VIKUNJA_CACHE_MAXELEMENTSIZE`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## redis
|
||||
|
@ -281,24 +466,44 @@ Whether to enable redis or not
|
|||
|
||||
Default: `false`
|
||||
|
||||
Full path: `redis.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_REDIS_ENABLED`
|
||||
|
||||
|
||||
### host
|
||||
|
||||
The host of the redis server including its port.
|
||||
|
||||
Default: `localhost:6379`
|
||||
|
||||
Full path: `redis.host`
|
||||
|
||||
Environment path: `VIKUNJA_REDIS_HOST`
|
||||
|
||||
|
||||
### password
|
||||
|
||||
The password used to authenicate against the redis server
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `redis.password`
|
||||
|
||||
Environment path: `VIKUNJA_REDIS_PASSWORD`
|
||||
|
||||
|
||||
### db
|
||||
|
||||
0 means default database
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `redis.db`
|
||||
|
||||
Environment path: `VIKUNJA_REDIS_DB`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## cors
|
||||
|
@ -313,18 +518,33 @@ Note: If you want to put the frontend and the api on seperate domains or ports,
|
|||
|
||||
Default: `true`
|
||||
|
||||
Full path: `cors.enable`
|
||||
|
||||
Environment path: `VIKUNJA_CORS_ENABLE`
|
||||
|
||||
|
||||
### origins
|
||||
|
||||
A list of origins which may access the api. These need to include the protocol (`http://` or `https://`) and port, if any.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `cors.origins`
|
||||
|
||||
Environment path: `VIKUNJA_CORS_ORIGINS`
|
||||
|
||||
|
||||
### maxage
|
||||
|
||||
How long (in seconds) the results of a preflight request can be cached.
|
||||
|
||||
Default: `0`
|
||||
|
||||
Full path: `cors.maxage`
|
||||
|
||||
Environment path: `VIKUNJA_CORS_MAXAGE`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## mailer
|
||||
|
@ -337,60 +557,110 @@ Whether to enable the mailer or not. If it is disabled, all users are enabled ri
|
|||
|
||||
Default: `false`
|
||||
|
||||
Full path: `mailer.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_ENABLED`
|
||||
|
||||
|
||||
### host
|
||||
|
||||
SMTP Host
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `mailer.host`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_HOST`
|
||||
|
||||
|
||||
### port
|
||||
|
||||
SMTP Host port
|
||||
|
||||
Default: `587`
|
||||
|
||||
Full path: `mailer.port`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_PORT`
|
||||
|
||||
|
||||
### username
|
||||
|
||||
SMTP username
|
||||
|
||||
Default: `user`
|
||||
|
||||
Full path: `mailer.username`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_USERNAME`
|
||||
|
||||
|
||||
### password
|
||||
|
||||
SMTP password
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `mailer.password`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_PASSWORD`
|
||||
|
||||
|
||||
### skiptlsverify
|
||||
|
||||
Wether to skip verification of the tls certificate on the server
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `mailer.skiptlsverify`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_SKIPTLSVERIFY`
|
||||
|
||||
|
||||
### fromemail
|
||||
|
||||
The default from address when sending emails
|
||||
|
||||
Default: `mail@vikunja`
|
||||
|
||||
Full path: `mailer.fromemail`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_FROMEMAIL`
|
||||
|
||||
|
||||
### queuelength
|
||||
|
||||
The length of the mail queue.
|
||||
|
||||
Default: `100`
|
||||
|
||||
Full path: `mailer.queuelength`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_QUEUELENGTH`
|
||||
|
||||
|
||||
### queuetimeout
|
||||
|
||||
The timeout in seconds after which the current open connection to the mailserver will be closed.
|
||||
|
||||
Default: `30`
|
||||
|
||||
Full path: `mailer.queuetimeout`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_QUEUETIMEOUT`
|
||||
|
||||
|
||||
### forcessl
|
||||
|
||||
By default, vikunja will try to connect with starttls, use this option to force it to use ssl.
|
||||
|
||||
Default: `false`
|
||||
|
||||
Full path: `mailer.forcessl`
|
||||
|
||||
Environment path: `VIKUNJA_MAILER_FORCESSL`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## log
|
||||
|
@ -403,60 +673,110 @@ A folder where all the logfiles should go.
|
|||
|
||||
Default: `<rootpath>logs`
|
||||
|
||||
Full path: `log.path`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_PATH`
|
||||
|
||||
|
||||
### enabled
|
||||
|
||||
Whether to show any logging at all or none
|
||||
|
||||
Default: `true`
|
||||
|
||||
Full path: `log.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_ENABLED`
|
||||
|
||||
|
||||
### standard
|
||||
|
||||
Where the normal log should go. Possible values are stdout, stderr, file or off to disable standard logging.
|
||||
|
||||
Default: `stdout`
|
||||
|
||||
Full path: `log.standard`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_STANDARD`
|
||||
|
||||
|
||||
### level
|
||||
|
||||
Change the log level. Possible values (case-insensitive) are CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG.
|
||||
|
||||
Default: `INFO`
|
||||
|
||||
Full path: `log.level`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_LEVEL`
|
||||
|
||||
|
||||
### database
|
||||
|
||||
Whether or not to log database queries. Useful for debugging. Possible values are stdout, stderr, file or off to disable database logging.
|
||||
|
||||
Default: `off`
|
||||
|
||||
Full path: `log.database`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_DATABASE`
|
||||
|
||||
|
||||
### databaselevel
|
||||
|
||||
The log level for database log messages. Possible values (case-insensitive) are CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG.
|
||||
|
||||
Default: `WARNING`
|
||||
|
||||
Full path: `log.databaselevel`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_DATABASELEVEL`
|
||||
|
||||
|
||||
### http
|
||||
|
||||
Whether to log http requests or not. Possible values are stdout, stderr, file or off to disable http logging.
|
||||
|
||||
Default: `stdout`
|
||||
|
||||
Full path: `log.http`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_HTTP`
|
||||
|
||||
|
||||
### echo
|
||||
|
||||
Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
|
||||
|
||||
Default: `off`
|
||||
|
||||
Full path: `log.echo`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_ECHO`
|
||||
|
||||
|
||||
### events
|
||||
|
||||
Whether or not to log events. Useful for debugging. Possible values are stdout, stderr, file or off to disable events logging.
|
||||
|
||||
Default: `stdout`
|
||||
|
||||
Full path: `log.events`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_EVENTS`
|
||||
|
||||
|
||||
### eventslevel
|
||||
|
||||
The log level for event log messages. Possible values (case-insensitive) are ERROR, INFO, DEBUG.
|
||||
|
||||
Default: `info`
|
||||
|
||||
Full path: `log.eventslevel`
|
||||
|
||||
Environment path: `VIKUNJA_LOG_EVENTSLEVEL`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ratelimit
|
||||
|
@ -469,24 +789,44 @@ whether or not to enable the rate limit
|
|||
|
||||
Default: `false`
|
||||
|
||||
Full path: `ratelimit.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_RATELIMIT_ENABLED`
|
||||
|
||||
|
||||
### kind
|
||||
|
||||
The kind on which rates are based. Can be either "user" for a rate limit per user or "ip" for an ip-based rate limit.
|
||||
|
||||
Default: `user`
|
||||
|
||||
Full path: `ratelimit.kind`
|
||||
|
||||
Environment path: `VIKUNJA_RATELIMIT_KIND`
|
||||
|
||||
|
||||
### period
|
||||
|
||||
The time period in seconds for the limit
|
||||
|
||||
Default: `60`
|
||||
|
||||
Full path: `ratelimit.period`
|
||||
|
||||
Environment path: `VIKUNJA_RATELIMIT_PERIOD`
|
||||
|
||||
|
||||
### limit
|
||||
|
||||
The max number of requests a user is allowed to do in the configured time period
|
||||
|
||||
Default: `100`
|
||||
|
||||
Full path: `ratelimit.limit`
|
||||
|
||||
Environment path: `VIKUNJA_RATELIMIT_LIMIT`
|
||||
|
||||
|
||||
### store
|
||||
|
||||
The store where the limit counter for each user is stored.
|
||||
|
@ -495,6 +835,11 @@ When choosing "keyvalue" this setting follows the one configured in the "keyvalu
|
|||
|
||||
Default: `keyvalue`
|
||||
|
||||
Full path: `ratelimit.store`
|
||||
|
||||
Environment path: `VIKUNJA_RATELIMIT_STORE`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## files
|
||||
|
@ -507,6 +852,11 @@ The path where files are stored
|
|||
|
||||
Default: `./files`
|
||||
|
||||
Full path: `files.basepath`
|
||||
|
||||
Environment path: `VIKUNJA_FILES_BASEPATH`
|
||||
|
||||
|
||||
### maxsize
|
||||
|
||||
The maximum size of a file, as a human-readable string.
|
||||
|
@ -514,6 +864,11 @@ Warning: The max size is limited 2^64-1 bytes due to the underlying datatype
|
|||
|
||||
Default: `20MB`
|
||||
|
||||
Full path: `files.maxsize`
|
||||
|
||||
Environment path: `VIKUNJA_FILES_MAXSIZE`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## migration
|
||||
|
@ -526,18 +881,38 @@ These are the settings for the wunderlist migrator
|
|||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `migration.wunderlist`
|
||||
|
||||
Environment path: `VIKUNJA_MIGRATION_WUNDERLIST`
|
||||
|
||||
|
||||
### todoist
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `migration.todoist`
|
||||
|
||||
Environment path: `VIKUNJA_MIGRATION_TODOIST`
|
||||
|
||||
|
||||
### trello
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `migration.trello`
|
||||
|
||||
Environment path: `VIKUNJA_MIGRATION_TRELLO`
|
||||
|
||||
|
||||
### microsofttodo
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `migration.microsofttodo`
|
||||
|
||||
Environment path: `VIKUNJA_MIGRATION_MICROSOFTTODO`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## avatar
|
||||
|
@ -550,6 +925,11 @@ When using gravatar, this is the duration in seconds until a cached gravatar use
|
|||
|
||||
Default: `3600`
|
||||
|
||||
Full path: `avatar.gravatarexpiration`
|
||||
|
||||
Environment path: `VIKUNJA_AVATAR_GRAVATAREXPIRATION`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## backgrounds
|
||||
|
@ -562,10 +942,20 @@ Whether to enable backgrounds for lists at all.
|
|||
|
||||
Default: `true`
|
||||
|
||||
Full path: `backgrounds.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_BACKGROUNDS_ENABLED`
|
||||
|
||||
|
||||
### providers
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `backgrounds.providers`
|
||||
|
||||
Environment path: `VIKUNJA_BACKGROUNDS_PROVIDERS`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## legal
|
||||
|
@ -579,10 +969,20 @@ Will be shown in the frontend if configured here
|
|||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `legal.imprinturl`
|
||||
|
||||
Environment path: `VIKUNJA_LEGAL_IMPRINTURL`
|
||||
|
||||
|
||||
### privacyurl
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `legal.privacyurl`
|
||||
|
||||
Environment path: `VIKUNJA_LEGAL_PRIVACYURL`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## keyvalue
|
||||
|
@ -598,6 +998,11 @@ The type of the storage backend. Can be either "memory" or "redis". If "redis" i
|
|||
|
||||
Default: `memory`
|
||||
|
||||
Full path: `keyvalue.type`
|
||||
|
||||
Environment path: `VIKUNJA_KEYVALUE_TYPE`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## auth
|
||||
|
@ -611,17 +1016,29 @@ This is the default auth mechanism and does not require any additional configura
|
|||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `auth.local`
|
||||
|
||||
Environment path: `VIKUNJA_AUTH_LOCAL`
|
||||
|
||||
|
||||
### openid
|
||||
|
||||
OpenID configuration will allow users to authenticate through a third-party OpenID Connect compatible provider.<br/>
|
||||
The provider needs to support the `openid`, `profile` and `email` scopes.<br/>
|
||||
**Note:** The frontend expects to be redirected after authentication by the third party
|
||||
**Note:** Some openid providers (like gitlab) only make the email of the user available through openid claims if they have set it to be publicly visible.
|
||||
If the email is not public in those cases, authenticating will fail.
|
||||
**Note 2:** The frontend expects to be redirected after authentication by the third party
|
||||
to <frontend-url>/auth/openid/<auth key>. Please make sure to configure the redirect url with your third party
|
||||
auth service accordingy if you're using the default vikunja frontend.
|
||||
Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/master/config.yml.sample) for more information about how to configure openid authentication.
|
||||
Take a look at the [default config file](https://kolaente.dev/vikunja/api/src/branch/main/config.yml.sample) for more information about how to configure openid authentication.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `auth.openid`
|
||||
|
||||
Environment path: `VIKUNJA_AUTH_OPENID`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## metrics
|
||||
|
@ -636,15 +1053,30 @@ If set to true, enables a /metrics endpoint for prometheus to collect metrics ab
|
|||
|
||||
Default: `false`
|
||||
|
||||
Full path: `metrics.enabled`
|
||||
|
||||
Environment path: `VIKUNJA_METRICS_ENABLED`
|
||||
|
||||
|
||||
### username
|
||||
|
||||
If set to a non-empty value the /metrics endpoint will require this as a username via basic auth in combination with the password below.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `metrics.username`
|
||||
|
||||
Environment path: `VIKUNJA_METRICS_USERNAME`
|
||||
|
||||
|
||||
### password
|
||||
|
||||
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.
|
||||
|
||||
Default: `<empty>`
|
||||
|
||||
Full path: `metrics.password`
|
||||
|
||||
Environment path: `VIKUNJA_METRICS_PASSWORD`
|
||||
|
||||
|
||||
|
|
|
@ -36,6 +36,8 @@ services:
|
|||
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
|
||||
|
@ -44,9 +46,9 @@ services:
|
|||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: root
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
|
|
|
@ -203,6 +203,8 @@ services:
|
|||
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
|
||||
|
@ -211,9 +213,9 @@ services:
|
|||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: root
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
|
@ -258,7 +260,9 @@ services:
|
|||
image: mariadb:10
|
||||
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_USER: vikunja
|
||||
MYSQL_PASSWORD: secret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
|
@ -267,9 +271,9 @@ services:
|
|||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: root
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
|
|
|
@ -28,7 +28,7 @@ wget <download-url>
|
|||
### Verify the GPG signature
|
||||
|
||||
Starting with version `0.7`, all releases are signed using pgp.
|
||||
Releases from `master` will always be signed.
|
||||
Releases from `main` will always be signed.
|
||||
|
||||
To validate the downloaded zip file use the signiture file `.asc` and the key `FF054DACD908493A`:
|
||||
|
||||
|
@ -147,9 +147,9 @@ services:
|
|||
image: vikunja/api:latest
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_PASSWORD: secret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: root
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_SERVICE_JWTSECRET: <generated secret>
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
|
@ -158,6 +158,8 @@ services:
|
|||
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
|
||||
|
@ -275,3 +277,8 @@ The API is now available through IP:
|
|||
## Configuration
|
||||
|
||||
See [available configuration options]({{< ref "config.md">}}).
|
||||
|
||||
## Default Password
|
||||
|
||||
After successfully installing Vikunja, there is no default user or password.
|
||||
You only need to register a new account and set all the details when creating it.
|
||||
|
|
|
@ -122,7 +122,7 @@ Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
|
|||
ServerName localhost
|
||||
DocumentRoot /path/to/vikunja/static/frontend/files
|
||||
RewriteEngine On
|
||||
RewriteRule ^\/?(config\.json|favicon\.ico|css|fonts|images|img|js) - [L]
|
||||
RewriteRule ^\/?(favicon\.ico|assets|audio|fonts|images|manifest\.webmanifest|robots\.txt|sw\.js|workbox-.*|api|dav|\.well-known) - [L]
|
||||
RewriteRule ^(.*)$ /index.html [QSA,L]
|
||||
</VirtualHost>
|
||||
{{< /highlight >}}
|
||||
|
|
|
@ -39,3 +39,15 @@ This document provides an overview and instructions for the different methods.
|
|||
* [Reverse proxies]({{< ref "reverse-proxies.md">}})
|
||||
* [Full docker example]({{< ref "full-docker-example.md">}})
|
||||
* [Backups]({{< ref "backups.md">}})
|
||||
|
||||
## Installation on kubernetes
|
||||
|
||||
A third-party Helm Chart is available from the k8s-at-home project [here](https://github.com/k8s-at-home/charts/tree/master/charts/stable/vikunja).
|
||||
|
||||
## Other installation resources
|
||||
|
||||
* [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/)
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@ Put the following config in `cat /etc/apache2/sites-available/vikunja.conf`:
|
|||
|
||||
DocumentRoot /var/www/html
|
||||
RewriteEngine On
|
||||
RewriteRule ^\/?(config\.json|favicon\.ico|css|fonts|images|img|js|api|dav|\.well-known) - [L]
|
||||
RewriteRule ^\/?(favicon\.ico|assets|audio|fonts|images|manifest\.webmanifest|robots\.txt|sw\.js|workbox-.*|api|dav|\.well-known) - [L]
|
||||
RewriteRule ^(.*)$ /index.html [QSA,L]
|
||||
</VirtualHost>
|
||||
{{< /highlight >}}
|
||||
|
|
|
@ -75,6 +75,7 @@ Vikunja **currently does not** support these properties:
|
|||
### Not working
|
||||
|
||||
* [Thunderbird (68)](https://www.thunderbird.net/)
|
||||
* iOS calDAV Sync (See [#753](https://kolaente.dev/vikunja/api/issues/753))
|
||||
|
||||
## Dev logs
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@ This document describes the different errors Vikunja can return.
|
|||
| 1016 | 412 | Totp is not enabled for this user. |
|
||||
| 1017 | 412 | The provided Totp passcode is invalid. |
|
||||
| 1018 | 412 | The provided user avatar provider type setting is invalid. |
|
||||
| 1019 | 412 | No openid email address was provided. |
|
||||
| 1020 | 412 | This user account is disabled. |
|
||||
|
||||
## Validation
|
||||
|
||||
|
|
64
go.mod
64
go.mod
|
@ -17,7 +17,7 @@
|
|||
module code.vikunja.io/api
|
||||
|
||||
require (
|
||||
code.vikunja.io/web v0.0.0-20210131201003-26386be9a9ae
|
||||
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
|
||||
|
@ -25,68 +25,58 @@ require (
|
|||
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 v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.0.0
|
||||
github.com/cweill/gotests v1.6.0
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0
|
||||
github.com/gabriel-vasile/mimetype v1.3.0
|
||||
github.com/getsentry/sentry-go v0.10.0
|
||||
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.6.0
|
||||
github.com/go-redis/redis/v8 v8.11.3
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.6.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.6.1
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/golang/snappy v0.0.2 // indirect
|
||||
github.com/iancoleman/strcase v0.1.3
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.3.0
|
||||
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.1
|
||||
github.com/lib/pq v1.10.3
|
||||
github.com/magefile/mage v1.11.0
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/mitchellh/mapstructure v1.3.2 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.8
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.10.0
|
||||
github.com/prometheus/client_golang v1.11.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/cast v1.3.1 // indirect
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.7.1
|
||||
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.0
|
||||
github.com/swaggo/swag v1.7.1
|
||||
github.com/ulule/limiter/v3 v3.8.0
|
||||
github.com/yuin/goldmark v1.3.5
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e
|
||||
golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c
|
||||
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/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/tools v0.1.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/ini.v1 v1.57.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c
|
||||
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.8
|
||||
xorm.io/builder v0.3.9
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.0.7
|
||||
xorm.io/xorm v1.1.2
|
||||
)
|
||||
|
||||
replace (
|
||||
|
|
24
magefile.go
24
magefile.go
|
@ -341,7 +341,7 @@ type Test mg.Namespace
|
|||
func (Test) Unit() {
|
||||
mg.Deps(initVars)
|
||||
// We run everything sequentially and not in parallel to prevent issues with real test databases
|
||||
args := append([]string{"test", Goflags[0], "-p", "1"}, ApiPackages...)
|
||||
args := append([]string{"test", Goflags[0], "-p", "1", "-timeout", "20m"}, ApiPackages...)
|
||||
runAndStreamOutput("go", args...)
|
||||
}
|
||||
|
||||
|
@ -356,7 +356,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", PACKAGE+"/pkg/integrations")
|
||||
runAndStreamOutput("go", "test", Goflags[0], "-p", "1", "-timeout", "20m", PACKAGE+"/pkg/integrations")
|
||||
}
|
||||
|
||||
type Check mg.Namespace
|
||||
|
@ -995,13 +995,20 @@ func parseYamlConfigNode(node *yaml.Node) (config *configOption) {
|
|||
return config
|
||||
}
|
||||
|
||||
func printConfig(config []*configOption, level int) (rendered string) {
|
||||
func printConfig(config []*configOption, level int, parent string) (rendered string) {
|
||||
|
||||
// Keep track of what we already printed to prevent printing things twice
|
||||
printed := make(map[string]bool)
|
||||
|
||||
for _, option := range config {
|
||||
|
||||
// FIXME: Not a good solution. Ideally this would work without the level check, but since generating config
|
||||
// for more than two levels is currently broken anyway, I'll fix this after moving the config generation
|
||||
// to a better format than yaml.
|
||||
if level == 0 && option.key != "" {
|
||||
parent = option.key
|
||||
}
|
||||
|
||||
if option.key != "" {
|
||||
|
||||
// Filter out all config objects where the default value == key
|
||||
|
@ -1030,12 +1037,17 @@ func printConfig(config []*configOption, level int) (rendered string) {
|
|||
if option.defaultValue == "" {
|
||||
rendered += "<empty>"
|
||||
}
|
||||
rendered += "`\n"
|
||||
rendered += "`\n\n"
|
||||
|
||||
fullPath := parent + "." + option.key
|
||||
|
||||
rendered += "Full path: `" + fullPath + "`\n\n"
|
||||
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(fullPath) + "`\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
printed[option.key] = true
|
||||
rendered += "\n" + printConfig(option.children, level+1)
|
||||
rendered += "\n" + printConfig(option.children, level+1, parent)
|
||||
}
|
||||
|
||||
return
|
||||
|
@ -1069,7 +1081,7 @@ func GenerateDocs() error {
|
|||
}
|
||||
}
|
||||
|
||||
renderedConfig := printConfig(conf, 0)
|
||||
renderedConfig := printConfig(conf, 0, "")
|
||||
|
||||
// Rebuild the config
|
||||
file, err := os.OpenFile(configDocPath, os.O_RDWR, 0)
|
||||
|
|
|
@ -216,7 +216,7 @@ DURATION:PT` + fmt.Sprintf("%.6f", t.Duration.Hours()) + `H` + fmt.Sprintf("%.6f
|
|||
|
||||
if t.Priority != 0 {
|
||||
caldavtodos += `
|
||||
PRIORITY:` + strconv.Itoa(int(t.Priority))
|
||||
PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority))
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
|
|
|
@ -375,6 +375,39 @@ COMPLETED:20181201T013024
|
|||
STATUS:COMPLETED
|
||||
LAST-MODIFIED:00010101T000000
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "with priority",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
},
|
||||
todos: []*Todo{
|
||||
{
|
||||
Summary: "Todo #1",
|
||||
Description: "Lorem Ipsum",
|
||||
UID: "randommduid",
|
||||
Priority: 1,
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavtasks: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
PRIORITY:9
|
||||
LAST-MODIFIED:00010101T000000
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -21,21 +21,20 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/caldav"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/laurent22/ical-go"
|
||||
)
|
||||
|
||||
func getCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string {
|
||||
func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.TaskWithComments) string {
|
||||
|
||||
// Make caldav todos from Vikunja todos
|
||||
var caldavtodos []*caldav.Todo
|
||||
var caldavtodos []*Todo
|
||||
for _, t := range listTasks {
|
||||
|
||||
duration := t.EndDate.Sub(t.StartDate)
|
||||
|
||||
caldavtodos = append(caldavtodos, &caldav.Todo{
|
||||
caldavtodos = append(caldavtodos, &Todo{
|
||||
Timestamp: t.Updated,
|
||||
UID: t.UID,
|
||||
Summary: t.Title,
|
||||
|
@ -52,15 +51,15 @@ func getCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string
|
|||
})
|
||||
}
|
||||
|
||||
caldavConfig := &caldav.Config{
|
||||
caldavConfig := &Config{
|
||||
Name: list.Title,
|
||||
ProdID: "Vikunja Todo App",
|
||||
}
|
||||
|
||||
return caldav.ParseTodos(caldavConfig, caldavtodos)
|
||||
return ParseTodos(caldavConfig, caldavtodos)
|
||||
}
|
||||
|
||||
func parseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
||||
func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
||||
parsed, err := ical.ParseCalendar(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -78,13 +77,15 @@ func parseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Parse the UID
|
||||
// Parse the priority
|
||||
var priority int64
|
||||
if _, ok := task["PRIORITY"]; ok {
|
||||
priority, err = strconv.ParseInt(task["PRIORITY"], 10, 64)
|
||||
priorityParsed, err := strconv.ParseInt(task["PRIORITY"], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priority = parseVTODOPriority(priorityParsed)
|
||||
}
|
||||
|
||||
// Parse the enddate
|
||||
|
@ -118,7 +119,7 @@ func caldavTimeToTimestamp(tstring string) time.Time {
|
|||
return time.Time{}
|
||||
}
|
||||
|
||||
format := caldav.DateFormat
|
||||
format := DateFormat
|
||||
|
||||
if strings.HasSuffix(tstring, "Z") {
|
||||
format = `20060102T150405Z`
|
|
@ -0,0 +1,101 @@
|
|||
// 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 caldav
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
)
|
||||
|
||||
func TestParseTaskFromVTODO(t *testing.T) {
|
||||
type args struct {
|
||||
content string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantVTask *models.Task
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
LAST-MODIFIED:00010101T000000
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Title: "Todo #1",
|
||||
UID: "randomuid",
|
||||
Description: "Lorem Ipsum",
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With priority",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
PRIORITY:9
|
||||
LAST-MODIFIED:00010101T000000
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Title: "Todo #1",
|
||||
UID: "randomuid",
|
||||
Description: "Lorem Ipsum",
|
||||
Priority: 1,
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseTaskFromVTODO(tt.args.content)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseTaskFromVTODO() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if diff, equal := messagediff.PrettyDiff(got, tt.wantVTask); !equal {
|
||||
t.Errorf("ParseTaskFromVTODO() gotVTask = %v, want %v, diff = %s", got, tt.wantVTask, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// 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 caldav
|
||||
|
||||
// In caldav, priority values are an int from 0 to 9 where 1 is the highest priority and 9 the lowest. 0 is "unset".
|
||||
// Vikunja only has priorites from 0 to 5 where 0 is unset and 5 is the highest
|
||||
// See https://icalendar.org/iCalendar-RFC-5545/3-8-1-9-priority.html
|
||||
func mapPriorityToCaldav(priority int64) (caldavPriority int) {
|
||||
switch priority {
|
||||
case 0:
|
||||
return 0
|
||||
case 1: // Low
|
||||
return 9
|
||||
case 2: // Medium
|
||||
return 5
|
||||
case 3: // High
|
||||
return 3
|
||||
case 4: // Urgent
|
||||
return 2
|
||||
case 5: // DO NOW
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// See mapPriorityToCaldav
|
||||
func parseVTODOPriority(priority int64) (vikunjaPriority int64) {
|
||||
switch priority {
|
||||
case 0:
|
||||
return 0
|
||||
case 1:
|
||||
return 5
|
||||
case 2:
|
||||
return 4
|
||||
case 3:
|
||||
return 3
|
||||
case 4:
|
||||
return 3
|
||||
case 5:
|
||||
return 2
|
||||
case 6:
|
||||
return 1
|
||||
case 7:
|
||||
return 1
|
||||
case 8:
|
||||
return 1
|
||||
case 9:
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
// 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 caldav
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_parseVTODOPriority(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
priority int64
|
||||
want int64
|
||||
}{
|
||||
{
|
||||
name: "unset",
|
||||
priority: 0,
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "DO NOW",
|
||||
priority: 1,
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "urgent",
|
||||
priority: 2,
|
||||
want: 4,
|
||||
},
|
||||
{
|
||||
name: "high 1",
|
||||
priority: 3,
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "high 2",
|
||||
priority: 4,
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "medium",
|
||||
priority: 5,
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "low 1",
|
||||
priority: 6,
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "low 2",
|
||||
priority: 7,
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "low 3",
|
||||
priority: 8,
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "low 4",
|
||||
priority: 9,
|
||||
want: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotVikunjaPriority := parseVTODOPriority(tt.priority); gotVikunjaPriority != tt.want {
|
||||
t.Errorf("parseVTODOPriority() = %v, want %v", gotVikunjaPriority, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_mapPriorityToCaldav(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
priority int64
|
||||
wantCaldavPriority int
|
||||
}{
|
||||
{
|
||||
name: "unset",
|
||||
priority: 0,
|
||||
wantCaldavPriority: 0,
|
||||
},
|
||||
{
|
||||
name: "low",
|
||||
priority: 1,
|
||||
wantCaldavPriority: 9,
|
||||
},
|
||||
{
|
||||
name: "medium",
|
||||
priority: 2,
|
||||
wantCaldavPriority: 5,
|
||||
},
|
||||
{
|
||||
name: "high",
|
||||
priority: 3,
|
||||
wantCaldavPriority: 3,
|
||||
},
|
||||
{
|
||||
name: "urgent",
|
||||
priority: 4,
|
||||
wantCaldavPriority: 2,
|
||||
},
|
||||
{
|
||||
name: "DO NOW",
|
||||
priority: 5,
|
||||
wantCaldavPriority: 1,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotCaldavPriority := mapPriorityToCaldav(tt.priority); gotCaldavPriority != tt.wantCaldavPriority {
|
||||
t.Errorf("mapPriorityToCaldav() = %v, want %v", gotCaldavPriority, tt.wantCaldavPriority)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ var testmailCmd = &cobra.Command{
|
|||
Run: func(cmd *cobra.Command, args []string) {
|
||||
log.Info("Sending testmail...")
|
||||
message := notifications.NewMail().
|
||||
From(config.MailerFromEmail.GetString()).
|
||||
From("Vikunja <"+config.MailerFromEmail.GetString()+">").
|
||||
To(args[0]).
|
||||
Subject("Test from Vikunja").
|
||||
Line("This is a test mail!").
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
@ -42,6 +43,7 @@ var (
|
|||
userFlagResetPasswordDirectly bool
|
||||
userFlagEnableUser bool
|
||||
userFlagDisableUser bool
|
||||
userFlagDeleteNow bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -66,7 +68,10 @@ func init() {
|
|||
userChangeEnabledCmd.Flags().BoolVarP(&userFlagDisableUser, "disable", "d", false, "Disable the user.")
|
||||
userChangeEnabledCmd.Flags().BoolVarP(&userFlagEnableUser, "enable", "e", false, "Enable the user.")
|
||||
|
||||
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd)
|
||||
// User deletion flags
|
||||
userDeleteCmd.Flags().BoolVarP(&userFlagDeleteNow, "now", "n", false, "If provided, deletes the user immediately instead of sending them an email first.")
|
||||
|
||||
userCmd.AddCommand(userListCmd, userCreateCmd, userUpdateCmd, userResetPasswordCmd, userChangeEnabledCmd, userDeleteCmd)
|
||||
rootCmd.AddCommand(userCmd)
|
||||
}
|
||||
|
||||
|
@ -135,7 +140,7 @@ var userListCmd = &cobra.Command{
|
|||
"ID",
|
||||
"Username",
|
||||
"Email",
|
||||
"Active",
|
||||
"Status",
|
||||
"Created",
|
||||
"Updated",
|
||||
})
|
||||
|
@ -145,7 +150,7 @@ var userListCmd = &cobra.Command{
|
|||
strconv.FormatInt(u.ID, 10),
|
||||
u.Username,
|
||||
u.Email,
|
||||
strconv.FormatBool(u.IsActive),
|
||||
u.Status.String(),
|
||||
u.Created.Format(time.RFC3339),
|
||||
u.Updated.Format(time.RFC3339),
|
||||
})
|
||||
|
@ -277,11 +282,15 @@ var userChangeEnabledCmd = &cobra.Command{
|
|||
u := getUserFromArg(s, args[0])
|
||||
|
||||
if userFlagEnableUser {
|
||||
u.IsActive = true
|
||||
u.Status = user.StatusActive
|
||||
} else if userFlagDisableUser {
|
||||
u.IsActive = false
|
||||
u.Status = user.StatusDisabled
|
||||
} else {
|
||||
u.IsActive = !u.IsActive
|
||||
if u.Status == user.StatusActive {
|
||||
u.Status = user.StatusDisabled
|
||||
} else {
|
||||
u.Status = user.StatusActive
|
||||
}
|
||||
}
|
||||
_, err := user.UpdateUser(s, u)
|
||||
if err != nil {
|
||||
|
@ -293,6 +302,64 @@ var userChangeEnabledCmd = &cobra.Command{
|
|||
log.Fatalf("Error saving everything: %s", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User status successfully changed, user is now active: %t.\n", u.IsActive)
|
||||
fmt.Printf("User status successfully changed, status is now \"%s\"\n", u.Status)
|
||||
},
|
||||
}
|
||||
|
||||
var userDeleteCmd = &cobra.Command{
|
||||
Use: "delete [user id]",
|
||||
Short: "Delete an existing user.",
|
||||
Long: "Kick off the user deletion process. If call without the --now flag, this command will only trigger an email to the user in order for them to confirm and start the deletion process. With the flag the user is deleted immediately. USE WITH CAUTION.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
initialize.FullInit()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if userFlagDeleteNow {
|
||||
fmt.Println("You requested to delete the user immediately. Are you sure?")
|
||||
fmt.Println(`To confirm, please type "yes, I confirm" in all uppercase:`)
|
||||
|
||||
cr := bufio.NewReader(os.Stdin)
|
||||
text, err := cr.ReadString('\n')
|
||||
if err != nil {
|
||||
log.Fatalf("could not read confirmation message: %s", err)
|
||||
}
|
||||
if text != "YES, I CONFIRM\n" {
|
||||
log.Fatalf("invalid confirmation message")
|
||||
}
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
if err := s.Begin(); err != nil {
|
||||
log.Fatalf("Count not start transaction: %s", err)
|
||||
}
|
||||
|
||||
u := getUserFromArg(s, args[0])
|
||||
|
||||
if userFlagDeleteNow {
|
||||
err := models.DeleteUser(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Error removing the user: %s", err)
|
||||
}
|
||||
} else {
|
||||
err := user.RequestDeletion(s, u)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
log.Fatalf("Could not request user deletion: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
log.Fatalf("Error saving everything: %s", err)
|
||||
}
|
||||
|
||||
if userFlagDeleteNow {
|
||||
fmt.Println("User deleted successfully.")
|
||||
} else {
|
||||
fmt.Println("User scheduled for deletion successfully.")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
@ -29,7 +30,9 @@ import (
|
|||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/routes"
|
||||
"code.vikunja.io/api/pkg/swagger"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -37,6 +40,31 @@ func init() {
|
|||
rootCmd.AddCommand(webCmd)
|
||||
}
|
||||
|
||||
func setupUnixSocket(e *echo.Echo) error {
|
||||
path := config.ServiceUnixSocket.GetString()
|
||||
|
||||
// Remove old unix socket that may have remained after a crash
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if config.ServiceUnixSocketMode.Get() != nil {
|
||||
// Use Umask instead of Chmod to prevent insecure race condition
|
||||
// (no-op on Windows)
|
||||
mode := config.ServiceUnixSocketMode.GetInt()
|
||||
oldmask := utils.Umask(0o777 &^ mode)
|
||||
defer utils.Umask(oldmask)
|
||||
}
|
||||
|
||||
l, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Listener = l
|
||||
return nil
|
||||
}
|
||||
|
||||
var webCmd = &cobra.Command{
|
||||
Use: "web",
|
||||
Short: "Starts the rest api web server",
|
||||
|
@ -56,6 +84,12 @@ var webCmd = &cobra.Command{
|
|||
routes.RegisterRoutes(e)
|
||||
// Start server
|
||||
go func() {
|
||||
// Listen unix socket if needed (ServiceInterface will be ignored)
|
||||
if config.ServiceUnixSocket.GetString() != "" {
|
||||
if err := setupUnixSocket(e); err != nil {
|
||||
e.Logger.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := e.Start(config.ServiceInterface.GetString()); err != nil {
|
||||
e.Logger.Info("shutting down...")
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ const (
|
|||
// #nosec
|
||||
ServiceJWTSecret Key = `service.JWTSecret`
|
||||
ServiceInterface Key = `service.interface`
|
||||
ServiceUnixSocket Key = `service.unixsocket`
|
||||
ServiceUnixSocketMode Key = `service.unixsocketmode`
|
||||
ServiceFrontendurl Key = `service.frontendurl`
|
||||
ServiceEnableCaldav Key = `service.enablecaldav`
|
||||
ServiceRootpath Key = `service.rootpath`
|
||||
|
@ -54,6 +56,7 @@ const (
|
|||
ServiceSentryDsn Key = `service.sentrydsn`
|
||||
ServiceTestingtoken Key = `service.testingtoken`
|
||||
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
||||
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
||||
|
||||
AuthLocalEnabled Key = `auth.local.enabled`
|
||||
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
||||
|
@ -224,6 +227,7 @@ func InitDefaultConfig() {
|
|||
// Service
|
||||
ServiceJWTSecret.setDefault(random)
|
||||
ServiceInterface.setDefault(":3456")
|
||||
ServiceUnixSocket.setDefault("")
|
||||
ServiceFrontendurl.setDefault("")
|
||||
ServiceEnableCaldav.setDefault(true)
|
||||
|
||||
|
@ -243,6 +247,7 @@ func InitDefaultConfig() {
|
|||
ServiceEnableTaskComments.setDefault(true)
|
||||
ServiceEnableTotp.setDefault(true)
|
||||
ServiceEnableEmailReminders.setDefault(true)
|
||||
ServiceEnableUserDeletion.setDefault(true)
|
||||
|
||||
// Auth
|
||||
AuthLocalEnabled.setDefault(true)
|
||||
|
@ -361,10 +366,18 @@ func InitConfig() {
|
|||
RateLimitStore.Set(KeyvalueType.GetString())
|
||||
}
|
||||
|
||||
if ServiceFrontendurl.GetString() != "" && !strings.HasSuffix(ServiceFrontendurl.GetString(), "/") {
|
||||
ServiceFrontendurl.Set(ServiceFrontendurl.GetString() + "/")
|
||||
}
|
||||
|
||||
if AuthOpenIDRedirectURL.GetString() == "" {
|
||||
AuthOpenIDRedirectURL.Set(ServiceFrontendurl.GetString() + "auth/openid/")
|
||||
}
|
||||
|
||||
if MigrationTodoistRedirectURL.GetString() == "" {
|
||||
MigrationTodoistRedirectURL.Set(ServiceFrontendurl.GetString() + "migrate/todoist")
|
||||
}
|
||||
|
||||
if MigrationTrelloRedirectURL.GetString() == "" {
|
||||
MigrationTrelloRedirectURL.Set(ServiceFrontendurl.GetString() + "migrate/trello")
|
||||
}
|
||||
|
|
|
@ -47,6 +47,9 @@ func Dump() (data map[string][]byte, err error) {
|
|||
|
||||
// Restore restores a table with all its entries
|
||||
func Restore(table string, contents []map[string]interface{}) (err error) {
|
||||
if _, err := x.IsTableExist(table); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, content := range contents {
|
||||
if _, err := x.Table(table).Insert(content); err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
list_id: 1
|
||||
created_by_id: 1
|
||||
limit: 9999999 # This bucket has a limit we will never exceed in the tests to make sure the logic allows for buckets with limits
|
||||
position: 2
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 2
|
||||
|
@ -10,6 +11,7 @@
|
|||
list_id: 1
|
||||
created_by_id: 1
|
||||
limit: 3
|
||||
position: 1
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 3
|
||||
|
@ -17,6 +19,7 @@
|
|||
list_id: 1
|
||||
created_by_id: 1
|
||||
is_done_bucket: 1
|
||||
position: 3
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 4
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
- entity_id: 1
|
||||
user_id: 1
|
||||
kind: 1
|
||||
- entity_id: 15
|
||||
user_id: 6 # owner
|
||||
kind: 1
|
||||
- entity_id: 15
|
||||
user_id: 1
|
||||
kind: 1
|
||||
- entity_id: 34
|
||||
user_id: 13 # owner
|
||||
kind: 1
|
||||
- entity_id: 34
|
||||
user_id: 1
|
||||
kind: 1
|
||||
- entity_id: 23
|
||||
user_id: 12 # owner
|
||||
kind: 2
|
||||
- entity_id: 23
|
||||
user_id: 1
|
||||
kind: 2
|
|
@ -5,6 +5,7 @@
|
|||
identifier: test1
|
||||
owner_id: 1
|
||||
namespace_id: 1
|
||||
position: 3
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -14,6 +15,7 @@
|
|||
identifier: test2
|
||||
owner_id: 3
|
||||
namespace_id: 1
|
||||
position: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -23,6 +25,7 @@
|
|||
identifier: test3
|
||||
owner_id: 3
|
||||
namespace_id: 2
|
||||
position: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -32,6 +35,7 @@
|
|||
identifier: test4
|
||||
owner_id: 3
|
||||
namespace_id: 3
|
||||
position: 4
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -41,6 +45,7 @@
|
|||
identifier: test5
|
||||
owner_id: 5
|
||||
namespace_id: 5
|
||||
position: 5
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -50,6 +55,7 @@
|
|||
identifier: test6
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 6
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -59,6 +65,7 @@
|
|||
identifier: test7
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 7
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -68,6 +75,7 @@
|
|||
identifier: test8
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 8
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -77,6 +85,7 @@
|
|||
identifier: test9
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 9
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -86,6 +95,7 @@
|
|||
identifier: test10
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 10
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -95,6 +105,7 @@
|
|||
identifier: test11
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 11
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -104,6 +115,7 @@
|
|||
identifier: test12
|
||||
owner_id: 6
|
||||
namespace_id: 7
|
||||
position: 12
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -113,6 +125,7 @@
|
|||
identifier: test13
|
||||
owner_id: 6
|
||||
namespace_id: 8
|
||||
position: 13
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -122,6 +135,7 @@
|
|||
identifier: test14
|
||||
owner_id: 6
|
||||
namespace_id: 9
|
||||
position: 14
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -131,6 +145,7 @@
|
|||
identifier: test15
|
||||
owner_id: 6
|
||||
namespace_id: 10
|
||||
position: 15
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -140,6 +155,7 @@
|
|||
identifier: test16
|
||||
owner_id: 6
|
||||
namespace_id: 11
|
||||
position: 16
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -149,6 +165,7 @@
|
|||
identifier: test17
|
||||
owner_id: 6
|
||||
namespace_id: 12
|
||||
position: 17
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# This list is owned by user 7, and several other users have access to it via different methods.
|
||||
|
@ -160,6 +177,7 @@
|
|||
identifier: test18
|
||||
owner_id: 7
|
||||
namespace_id: 13
|
||||
position: 18
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -169,6 +187,7 @@
|
|||
identifier: test19
|
||||
owner_id: 7
|
||||
namespace_id: 14
|
||||
position: 19
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
# User 1 does not have access to this list
|
||||
|
@ -179,6 +198,7 @@
|
|||
identifier: test20
|
||||
owner_id: 13
|
||||
namespace_id: 15
|
||||
position: 20
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -188,6 +208,7 @@
|
|||
identifier: test21
|
||||
owner_id: 1
|
||||
namespace_id: 16
|
||||
position: 21
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -198,6 +219,7 @@
|
|||
owner_id: 1
|
||||
namespace_id: 1
|
||||
is_archived: 1
|
||||
position: 22
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
|
@ -207,6 +229,16 @@
|
|||
identifier: test23
|
||||
owner_id: 12
|
||||
namespace_id: 17
|
||||
is_favorite: true
|
||||
position: 23
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 24
|
||||
title: Test24
|
||||
description: Lorem Ipsum
|
||||
identifier: test6
|
||||
owner_id: 6
|
||||
namespace_id: 6
|
||||
position: 7
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 1
|
||||
is_favorite: true
|
||||
position: 2
|
||||
- id: 2
|
||||
title: 'task #2 done'
|
||||
done: true
|
||||
|
@ -18,6 +18,7 @@
|
|||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 1
|
||||
position: 4
|
||||
- id: 3
|
||||
title: 'task #3 high prio'
|
||||
done: false
|
||||
|
@ -141,7 +142,6 @@
|
|||
list_id: 6
|
||||
index: 1
|
||||
bucket_id: 6
|
||||
is_favorite: true
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 16
|
||||
|
@ -317,7 +317,6 @@
|
|||
list_id: 20
|
||||
index: 20
|
||||
bucket_id: 5
|
||||
is_favorite: true
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
- id: 35
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
-
|
||||
id: 1
|
||||
user_id: 3
|
||||
token: 'passwordresettesttoken'
|
||||
kind: 1
|
||||
created: 2021-07-12 00:00:11
|
||||
-
|
||||
id: 2
|
||||
user_id: 4
|
||||
token: 'tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael'
|
||||
kind: 2
|
||||
created: 2021-07-12 00:00:12
|
||||
-
|
||||
id: 3
|
||||
user_id: 5
|
||||
token: 'tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Aei'
|
||||
kind: 2
|
||||
created: 2021-07-12 00:00:13
|
|
@ -3,7 +3,6 @@
|
|||
username: 'user1'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user1@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -20,7 +19,6 @@
|
|||
username: 'user3'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user3@example.com'
|
||||
password_reset_token: passwordresettesttoken
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -29,7 +27,7 @@
|
|||
username: 'user4'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user4@example.com'
|
||||
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
|
||||
status: 1
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -38,8 +36,7 @@
|
|||
username: 'user5'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user5@example.com'
|
||||
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
|
||||
is_active: false
|
||||
status: 1
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -48,7 +45,6 @@
|
|||
username: 'user6'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user6@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -56,7 +52,6 @@
|
|||
username: 'user7'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user7@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
discoverable_by_email: true
|
||||
updated: 2018-12-02 15:13:12
|
||||
|
@ -65,7 +60,6 @@
|
|||
username: 'user8'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user8@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -73,7 +67,6 @@
|
|||
username: 'user9'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user9@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -81,7 +74,6 @@
|
|||
username: 'user10'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user10@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -90,7 +82,6 @@
|
|||
name: 'Some one else'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user11@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -99,7 +90,6 @@
|
|||
name: 'Name with spaces'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user12@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
discoverable_by_name: true
|
||||
updated: 2018-12-02 15:13:12
|
||||
|
@ -108,7 +98,6 @@
|
|||
username: 'user13'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user14@example.com'
|
||||
is_active: true
|
||||
issuer: local
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
|
@ -116,7 +105,6 @@
|
|||
username: 'user14'
|
||||
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
|
||||
email: 'user15@some.service.com'
|
||||
is_active: true
|
||||
issuer: 'https://some.service.com'
|
||||
subject: '12345'
|
||||
updated: 2018-12-02 15:13:12
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// 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 db
|
||||
|
||||
import (
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// ILIKE returns an ILIKE query on postgres and a LIKE query on all other platforms.
|
||||
// Postgres' is case sensitive by default.
|
||||
// To work around this, we're using ILIKE as opposed to normal LIKE statements.
|
||||
// ILIKE is preferred over LOWER(text) LIKE for performance reasons.
|
||||
// See https://stackoverflow.com/q/7005302/10924593
|
||||
func ILIKE(column, search string) builder.Cond {
|
||||
if Type() == schemas.POSTGRES {
|
||||
return builder.Expr(column+" ILIKE ?", "%"+search+"%")
|
||||
}
|
||||
|
||||
return &builder.Like{column, "%" + search + "%"}
|
||||
}
|
|
@ -59,10 +59,6 @@ func InitFixtures(tablenames ...string) (err error) {
|
|||
}
|
||||
|
||||
fixtures, err = testfixtures.New(loaderOptions...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -106,7 +102,7 @@ func LoadFixtures() error {
|
|||
}
|
||||
}
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAndAssertFixtures loads all fixtures defined before and asserts they are correctly loaded
|
||||
|
|
|
@ -17,8 +17,12 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -47,3 +51,13 @@ func AssertDispatched(t *testing.T, event Event) {
|
|||
|
||||
assert.True(t, found, "Failed to assert "+event.Name()+" has been dispatched.")
|
||||
}
|
||||
|
||||
// TestListener takes an event and a listener and calls the listener's Handle method.
|
||||
func TestListener(t *testing.T, event Event, listener Listener) {
|
||||
content, err := json.Marshal(event)
|
||||
assert.NoError(t, err)
|
||||
|
||||
msg := message.NewMessage(watermill.NewUUID(), content)
|
||||
err = listener.Handle(msg)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -18,11 +18,15 @@ package files
|
|||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/c2h5oh/datasize"
|
||||
"github.com/spf13/afero"
|
||||
|
@ -75,7 +79,18 @@ func Create(f io.Reader, realname string, realsize uint64, a web.Auth) (file *Fi
|
|||
|
||||
// CreateWithMime creates a new file from an FileHeader and sets its mime type
|
||||
func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, mime string) (file *File, err error) {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
file, err = CreateWithMimeAndSession(s, f, realname, realsize, a, mime)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CreateWithMimeAndSession(s *xorm.Session, f io.Reader, realname string, realsize uint64, a web.Auth, mime string) (file *File, err error) {
|
||||
// Get and parse the configured file size
|
||||
var maxSize datasize.ByteSize
|
||||
err = maxSize.UnmarshalText([]byte(config.FilesMaxSize.GetString()))
|
||||
|
@ -94,21 +109,13 @@ func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, m
|
|||
Mime: mime,
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
_, err = s.Insert(file)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return
|
||||
}
|
||||
|
||||
// Save the file to storage with its new ID as path
|
||||
err = file.Save(f)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -129,9 +136,16 @@ func (f *File) Delete() (err error) {
|
|||
|
||||
err = afs.Remove(f.getFileName())
|
||||
if err != nil {
|
||||
if e, is := err.(*os.PathError); is {
|
||||
// Don't fail when removing the file failed
|
||||
log.Errorf("Error deleting file %d: %s", e.Error())
|
||||
return s.Commit()
|
||||
}
|
||||
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -94,6 +94,10 @@ func FullInit() {
|
|||
cron.Init()
|
||||
models.RegisterReminderCron()
|
||||
models.RegisterOverdueReminderCron()
|
||||
user.RegisterTokenCleanupCron()
|
||||
user.RegisterDeletionNotificationCron()
|
||||
models.RegisterUserDeletionCron()
|
||||
models.RegisterOldExportCleanupCron()
|
||||
|
||||
// Start processing events
|
||||
go func() {
|
||||
|
|
|
@ -24,18 +24,18 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"code.vikunja.io/api/pkg/routes"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -47,7 +47,6 @@ var (
|
|||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user1@example.com",
|
||||
IsActive: true,
|
||||
}
|
||||
testuser2 = user.User{
|
||||
ID: 2,
|
||||
|
@ -56,26 +55,23 @@ var (
|
|||
Email: "user2@example.com",
|
||||
}
|
||||
testuser3 = user.User{
|
||||
ID: 3,
|
||||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user3@example.com",
|
||||
PasswordResetToken: "passwordresettesttoken",
|
||||
ID: 3,
|
||||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user3@example.com",
|
||||
}
|
||||
testuser4 = user.User{
|
||||
ID: 4,
|
||||
Username: "user4",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user4@example.com",
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
ID: 4,
|
||||
Username: "user4",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user4@example.com",
|
||||
}
|
||||
testuser5 = user.User{
|
||||
ID: 4,
|
||||
Username: "user5",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user5@example.com",
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
IsActive: false,
|
||||
ID: 4,
|
||||
Username: "user5",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Email: "user5@example.com",
|
||||
Status: user.StatusDisabled,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -88,6 +84,7 @@ func setupTestEnv() (e *echo.Echo, err error) {
|
|||
user.InitTests()
|
||||
models.SetupTests()
|
||||
events.Fake()
|
||||
keyvalue.InitStorage()
|
||||
|
||||
err = db.LoadFixtures()
|
||||
if err != nil {
|
||||
|
|
|
@ -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,"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,"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,"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,"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"}}]`)
|
||||
})
|
||||
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,"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,"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,"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,"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,"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,"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"}}]`)
|
||||
})
|
||||
// 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,"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,"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,"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"}}]`)
|
||||
})
|
||||
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,"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,"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,"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,"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,"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"}}]`)
|
||||
})
|
||||
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,"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,"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,"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"}}]`)
|
||||
})
|
||||
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,"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,"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,"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,"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,"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"}}]`)
|
||||
})
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
|
@ -341,33 +341,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,"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,"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,"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,"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"}}]`)
|
||||
})
|
||||
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,"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,"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,"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,"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,"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,"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"}}]`)
|
||||
})
|
||||
// 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,"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,"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,"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"}}]`)
|
||||
})
|
||||
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,"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,"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,"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,"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,"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"}}]`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
|
|
|
@ -67,21 +67,24 @@ func StartMailDaemon() {
|
|||
if !open {
|
||||
if s, err = d.Dial(); err != nil {
|
||||
log.Error("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)
|
||||
break
|
||||
}
|
||||
// Close the connection to the SMTP server if no email was sent in
|
||||
// the last 30 seconds.
|
||||
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)
|
||||
break
|
||||
}
|
||||
log.Infof("Closed connection to mailserver")
|
||||
open = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ func SendTestMail(opts *Opts) error {
|
|||
func sendMail(opts *Opts) *gomail.Message {
|
||||
m := gomail.NewMessage()
|
||||
if opts.From == "" {
|
||||
opts.From = config.MailerFromEmail.GetString()
|
||||
opts.From = "Vikunja <" + config.MailerFromEmail.GetString() + ">"
|
||||
}
|
||||
m.SetHeader("From", opts.From)
|
||||
m.SetHeader("To", opts.To)
|
||||
|
|
|
@ -91,12 +91,8 @@ func SetUserActive(a web.Auth) (err error) {
|
|||
|
||||
// getActiveUsers returns the active users from redis
|
||||
func getActiveUsers() (users activeUsersMap, err error) {
|
||||
u, _, err := keyvalue.Get(ActiveUsersKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users = u.(activeUsersMap)
|
||||
users = activeUsersMap{}
|
||||
_, err = keyvalue.GetWithValue(ActiveUsersKey, &users)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,13 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
|
@ -45,8 +49,8 @@ var registry *prometheus.Registry
|
|||
func GetRegistry() *prometheus.Registry {
|
||||
if registry == nil {
|
||||
registry = prometheus.NewRegistry()
|
||||
registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
|
||||
registry.MustRegister(prometheus.NewGoCollector())
|
||||
registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
|
||||
registry.MustRegister(collectors.NewGoCollector())
|
||||
}
|
||||
|
||||
return registry
|
||||
|
@ -132,7 +136,11 @@ func GetCount(key string) (count int64, err error) {
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
count = cnt.(int64)
|
||||
if s, is := cnt.(string); is {
|
||||
count, err = strconv.ParseInt(s, 10, 64)
|
||||
} else {
|
||||
count = cnt.(int64)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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 users20210527105701 struct {
|
||||
DefaultListID int64 `xorm:"bigint null index" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210527105701) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210527105701",
|
||||
Description: "Add default list for new tasks setting to users",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210527105701{})
|
||||
},
|
||||
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 users20210603174608 struct {
|
||||
WeekStart int `xorm:"null" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210603174608) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210603174608",
|
||||
Description: "Add week start user setting",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210603174608{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// 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"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210709191101",
|
||||
Description: "Make the task title type TEXT instead of varchar(250)",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
switch config.DatabaseType.GetString() {
|
||||
case "sqlite":
|
||||
// Sqlite only has a "TEXT" type so we don't need to modify it
|
||||
case "mysql":
|
||||
_, err := tx.Exec("alter table tasks modify title text not null")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "postgres":
|
||||
_, err := tx.Exec("alter table tasks alter column title type text using title::text")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
log.Fatal("Unknown db.")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// 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 favorites20210709211508 struct {
|
||||
EntityID int64 `xorm:"bigint not null pk"`
|
||||
UserID int64 `xorm:"bigint not null pk"`
|
||||
Kind int `xorm:"int not null pk"`
|
||||
}
|
||||
|
||||
func (favorites20210709211508) TableName() string {
|
||||
return "favorites"
|
||||
}
|
||||
|
||||
type task20210709211508 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
CreatedByID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the list
|
||||
}
|
||||
|
||||
func (task20210709211508) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
type list20210709211508 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
OwnerID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the list
|
||||
}
|
||||
|
||||
func (list20210709211508) TableName() string {
|
||||
return "lists"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210709211508",
|
||||
Description: "Move favorites to new table",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(favorites20210709211508{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migrate all existing favorites
|
||||
tasks := []*task20210709211508{}
|
||||
err = tx.Where("is_favorite = ?", true).Find(&tasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
fav := &favorites20210709211508{
|
||||
EntityID: task.ID,
|
||||
UserID: task.CreatedByID,
|
||||
Kind: 1,
|
||||
}
|
||||
_, err = tx.Insert(fav)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
lists := []*list20210709211508{}
|
||||
err = tx.Where("is_favorite = ?", true).Find(&lists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, list := range lists {
|
||||
fav := &favorites20210709211508{
|
||||
EntityID: list.ID,
|
||||
UserID: list.OwnerID,
|
||||
Kind: 2,
|
||||
}
|
||||
_, err = tx.Insert(fav)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = dropTableColum(tx, "tasks", "is_favorite")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dropTableColum(tx, "lists", "is_favorite")
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type user20210711173657 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"`
|
||||
PasswordResetToken string `xorm:"varchar(450) null" json:"-"`
|
||||
EmailConfirmToken string `xorm:"varchar(450) null" json:"-"`
|
||||
}
|
||||
|
||||
func (u user20210711173657) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
type userTokens20210711173657 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk"`
|
||||
UserID int64 `xorm:"not null"`
|
||||
Token string `xorm:"varchar(450) not null index"`
|
||||
Kind int `xorm:"not null"`
|
||||
Created time.Time `xorm:"created not null"`
|
||||
}
|
||||
|
||||
func (userTokens20210711173657) TableName() string {
|
||||
return "user_tokens"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210711173657",
|
||||
Description: "Add user tokens table",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
_ = tx.DropTables(&userTokens20210711173657{}) // Allow running this migration multiple times
|
||||
|
||||
err := tx.Sync2(userTokens20210711173657{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users := []*user20210711173657{}
|
||||
err = tx.Where(`password_reset_token != '' OR email_confirm_token != ''`).Find(&users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const tokenPasswordReset = 1
|
||||
const tokenEmailConfirm = 2
|
||||
|
||||
for _, user := range users {
|
||||
if user.PasswordResetToken != "" {
|
||||
_, err = tx.Insert(&userTokens20210711173657{
|
||||
UserID: user.ID,
|
||||
Token: user.PasswordResetToken,
|
||||
Kind: tokenPasswordReset,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if user.EmailConfirmToken != "" {
|
||||
_, err = tx.Insert(&userTokens20210711173657{
|
||||
UserID: user.ID,
|
||||
Token: user.EmailConfirmToken,
|
||||
Kind: tokenEmailConfirm,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = dropTableColum(tx, "users", "password_reset_token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dropTableColum(tx, "users", "email_confirm_token")
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// 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 users20210713213622 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id"`
|
||||
IsActive bool `xorm:"null" json:"-"`
|
||||
Status int `xorm:"default 0" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210713213622) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210713213622",
|
||||
Description: "Add users status instead of is_active",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(users20210713213622{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users := []*users20210713213622{}
|
||||
err = tx.Find(&users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
if user.IsActive {
|
||||
continue
|
||||
}
|
||||
|
||||
user.Status = 1 // 1 is "email confirmation required" - as that's the only way is_active was used before we'll use that
|
||||
_, err := tx.
|
||||
Where("id = ?", user.ID).
|
||||
Cols("status").
|
||||
Update(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return dropTableColum(tx, "users", "is_active")
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// 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 (
|
||||
"math"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type tasks20210725153703 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id" param:"listtask"`
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
KanbanPosition float64 `xorm:"double null" json:"kanban_position"`
|
||||
}
|
||||
|
||||
func (tasks20210725153703) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210725153703",
|
||||
Description: "",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(tasks20210725153703{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tasks := []*tasks20210725153703{}
|
||||
err = tx.Where("position is not null").Find(&tasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migrate all old kanban positions to the kanban positions property
|
||||
for _, task := range tasks {
|
||||
task.KanbanPosition = task.Position
|
||||
task.Position = float64(task.ID) * math.Pow(2, 16)
|
||||
_, err = tx.
|
||||
Where("id = ?", task.ID).
|
||||
Cols("kanban_position", "position").
|
||||
Update(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// 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 (
|
||||
"math"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type lists20210727204942 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id" param:"list"`
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
}
|
||||
|
||||
func (lists20210727204942) TableName() string {
|
||||
return "lists"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210727204942",
|
||||
Description: "Add list position parameter",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(lists20210727204942{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lists := []*lists20210727204942{}
|
||||
err = tx.Find(&lists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, list := range lists {
|
||||
list.Position = float64(list.ID) * math.Pow(2, 16)
|
||||
|
||||
_, err = tx.
|
||||
Where("id = ?", list.ID).
|
||||
Update(list)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// 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 (
|
||||
"math"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type buckets20210727211037 struct {
|
||||
ID int64 `xorm:"bigint autoincr not null" json:"id" param:"list"`
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
}
|
||||
|
||||
func (buckets20210727211037) TableName() string {
|
||||
return "buckets"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210727211037",
|
||||
Description: "Add bucket position property",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(buckets20210727211037{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buckets := []*buckets20210727211037{}
|
||||
err = tx.Find(&buckets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, bucket := range buckets {
|
||||
bucket.Position = float64(bucket.ID) * math.Pow(2, 16)
|
||||
|
||||
_, err = tx.
|
||||
Where("id = ?", bucket.ID).
|
||||
Update(bucket)
|
||||
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 notifications20210729142940 struct {
|
||||
SubjectID int64 `xorm:"bigint null" json:"-"`
|
||||
}
|
||||
|
||||
func (notifications20210729142940) TableName() string {
|
||||
return "notifications"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210729142940",
|
||||
Description: "Add subject id to notification",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(notifications20210729142940{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// 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 (
|
||||
"time"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type users20210802081716 struct {
|
||||
DeletionScheduledAt time.Time `xorm:"datetime null" json:"-"`
|
||||
DeletionLastReminderSent time.Time `xorm:"datetime null" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210802081716) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210802081716",
|
||||
Description: "Add account deletion schedule timestamps",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210802081716{})
|
||||
},
|
||||
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 users20210829194722 struct {
|
||||
ExportFileID int64 `xorm:"bigint null" json:"-"`
|
||||
}
|
||||
|
||||
func (users20210829194722) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20210829194722",
|
||||
Description: "Add data export file id to users",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20210829194722{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -21,6 +21,16 @@ import (
|
|||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
// DataExportRequestEvent represents a DataExportRequestEvent event
|
||||
type DataExportRequestEvent struct {
|
||||
User *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for DataExportRequestEvent
|
||||
func (t *DataExportRequestEvent) Name() string {
|
||||
return "user.export.request"
|
||||
}
|
||||
|
||||
/////////////////
|
||||
// Task Events //
|
||||
/////////////////
|
||||
|
@ -82,6 +92,18 @@ func (t *TaskCommentCreatedEvent) Name() string {
|
|||
return "task.comment.created"
|
||||
}
|
||||
|
||||
// TaskCommentUpdatedEvent represents a TaskCommentUpdatedEvent event
|
||||
type TaskCommentUpdatedEvent struct {
|
||||
Task *Task
|
||||
Comment *TaskComment
|
||||
Doer *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for TaskCommentUpdatedEvent
|
||||
func (t *TaskCommentUpdatedEvent) Name() string {
|
||||
return "task.comment.edited"
|
||||
}
|
||||
|
||||
//////////////////////
|
||||
// Namespace Events //
|
||||
//////////////////////
|
||||
|
@ -245,3 +267,13 @@ type TeamDeletedEvent struct {
|
|||
func (t *TeamDeletedEvent) Name() string {
|
||||
return "team.deleted"
|
||||
}
|
||||
|
||||
// UserDataExportRequestedEvent represents a UserDataExportRequestedEvent event
|
||||
type UserDataExportRequestedEvent struct {
|
||||
User *user.User
|
||||
}
|
||||
|
||||
// Name defines the name for UserDataExportRequestedEvent
|
||||
func (t *UserDataExportRequestedEvent) Name() string {
|
||||
return "user.export.requested"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,366 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func ExportUserData(s *xorm.Session, u *user.User) (err error) {
|
||||
exportDir := config.FilesBasePath.GetString() + "/user-export-tmp/"
|
||||
err = os.MkdirAll(exportDir, 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpFilename := exportDir + strconv.FormatInt(u.ID, 10) + "_" + time.Now().Format("2006-01-02_15-03-05") + ".zip"
|
||||
|
||||
// Open zip
|
||||
dumpFile, err := os.Create(tmpFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening dump file: %s", err)
|
||||
}
|
||||
defer dumpFile.Close()
|
||||
|
||||
dumpWriter := zip.NewWriter(dumpFile)
|
||||
defer dumpWriter.Close()
|
||||
|
||||
// Get the data
|
||||
err = exportListsAndTasks(s, u, dumpWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Task attachment files
|
||||
err = exportTaskAttachments(s, u, dumpWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Saved filters
|
||||
err = exportSavedFilters(s, u, dumpWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Background files
|
||||
err = exportListBackgrounds(s, u, dumpWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Vikunja Version
|
||||
err = utils.WriteBytesToZip("VERSION", []byte(version.Version), dumpWriter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we reuse the same file again, saving it as a file in Vikunja will save it as a file with 0 bytes in size.
|
||||
// Closing and reopening does work.
|
||||
dumpWriter.Close()
|
||||
dumpFile.Close()
|
||||
|
||||
exported, err := os.Open(tmpFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stat, err := exported.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exportFile, err := files.CreateWithMimeAndSession(s, exported, tmpFilename, uint64(stat.Size()), u, "application/zip")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the file id with the user
|
||||
u.ExportFileID = exportFile.ID
|
||||
_, err = s.Cols("export_file_id").Update(u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the old file
|
||||
err = os.Remove(exported.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send a notification
|
||||
return notifications.Notify(u, &DataExportReadyNotification{
|
||||
User: u,
|
||||
})
|
||||
}
|
||||
|
||||
func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
|
||||
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
|
||||
continue
|
||||
}
|
||||
|
||||
nn := &NamespaceWithListsAndTasks{
|
||||
Namespace: n.Namespace,
|
||||
Lists: []*ListWithTasksAndBuckets{},
|
||||
}
|
||||
|
||||
for _, l := range n.Lists {
|
||||
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)
|
||||
namespaces = append(namespaces, nn)
|
||||
}
|
||||
|
||||
if len(namespaceIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get all lists
|
||||
lists, err := getListsForNamespaces(s, namespaceIDs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tasks, _, _, err := getTasksForLists(s, lists, u, &taskOptions{
|
||||
page: 0,
|
||||
perPage: -1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
comments := []*TaskComment{}
|
||||
err = s.
|
||||
Join("LEFT", "tasks", "tasks.id = task_comments.task_id").
|
||||
In("tasks.list_id", listIDs).
|
||||
Find(&comments)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
buckets := []*Bucket{}
|
||||
err = s.In("list_id", listIDs).Find(&buckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(namespaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.WriteBytesToZip("data.json", data, wr)
|
||||
}
|
||||
|
||||
func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
lists, _, _, err := getRawListsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
user: u,
|
||||
page: -1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tasks, _, _, err := getRawTasksForLists(s, lists, u, &taskOptions{page: -1})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
taskIDs := []int64{}
|
||||
for _, t := range tasks {
|
||||
taskIDs = append(taskIDs, t.ID)
|
||||
}
|
||||
|
||||
tas, err := getTaskAttachmentsByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fs := make(map[int64]io.ReadCloser)
|
||||
for _, ta := range tas {
|
||||
if err := ta.File.LoadFileByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
fs[ta.FileID] = ta.File.File
|
||||
}
|
||||
|
||||
return utils.WriteFilesToZip(fs, wr)
|
||||
}
|
||||
|
||||
func exportSavedFilters(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
filters, err := getSavedFiltersForUser(s, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return utils.WriteBytesToZip("filters.json", data, wr)
|
||||
}
|
||||
|
||||
func exportListBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
|
||||
lists, _, _, err := getRawListsForUser(
|
||||
s,
|
||||
&listOptions{
|
||||
user: u,
|
||||
page: -1,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fs := make(map[int64]io.ReadCloser)
|
||||
for _, l := range lists {
|
||||
if l.BackgroundFileID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
bgFile := &files.File{
|
||||
ID: l.BackgroundFileID,
|
||||
}
|
||||
err = bgFile.LoadFileByID()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fs[l.BackgroundFileID] = bgFile.File
|
||||
}
|
||||
|
||||
return utils.WriteFilesToZip(fs, wr)
|
||||
}
|
||||
|
||||
func RegisterOldExportCleanupCron() {
|
||||
const logPrefix = "[User Export Cleanup Cron] "
|
||||
|
||||
err := cron.Schedule("0 * * * *", func() {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
users := []*user.User{}
|
||||
err := s.Where("export_file_id IS NOT NULL AND export_file_id != ?", 0).Find(&users)
|
||||
if err != nil {
|
||||
log.Errorf(logPrefix+"Could not get users with export files: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileIDs := []int64{}
|
||||
for _, u := range users {
|
||||
fileIDs = append(fileIDs, u.ExportFileID)
|
||||
}
|
||||
|
||||
fs := []*files.File{}
|
||||
err = s.Where("created < ?", time.Now().Add(-time.Hour*24*7)).In("id", fileIDs).Find(&fs)
|
||||
if err != nil {
|
||||
log.Errorf(logPrefix+"Could not get users with export files: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(fs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf(logPrefix+"Removing %d old user data exports...", len(fs))
|
||||
|
||||
for _, f := range fs {
|
||||
err = f.Delete()
|
||||
if err != nil {
|
||||
log.Errorf(logPrefix+"Could not remove user export file %d: %s", f.ID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = s.In("export_file_id", fileIDs).Cols("export_file_id").Update(&user.User{})
|
||||
if err != nil {
|
||||
log.Errorf(logPrefix+"Could not update user export file state: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf(logPrefix+"Removed %d old user data exports...", len(fs))
|
||||
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not old export cleanup cron: %s", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// FavoriteKind represents the kind of entities that can be marked as favorite
|
||||
type FavoriteKind int
|
||||
|
||||
const (
|
||||
FavoriteKindUnknown FavoriteKind = iota
|
||||
FavoriteKindTask
|
||||
FavoriteKindList
|
||||
)
|
||||
|
||||
// Favorite represents an entity which is a favorite to someone
|
||||
type Favorite struct {
|
||||
EntityID int64 `xorm:"bigint not null pk"`
|
||||
UserID int64 `xorm:"bigint not null pk"`
|
||||
Kind FavoriteKind `xorm:"int not null pk"`
|
||||
}
|
||||
|
||||
// TableName is the table name
|
||||
func (t *Favorite) TableName() string {
|
||||
return "favorites"
|
||||
}
|
||||
|
||||
func addToFavorites(s *xorm.Session, entityID int64, a web.Auth, kind FavoriteKind) error {
|
||||
u, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
// Only error GetFromAuth is if it's a link share and we want to ignore that
|
||||
return nil
|
||||
}
|
||||
|
||||
fav := &Favorite{
|
||||
EntityID: entityID,
|
||||
UserID: u.ID,
|
||||
Kind: kind,
|
||||
}
|
||||
|
||||
_, err = s.Insert(fav)
|
||||
return err
|
||||
}
|
||||
|
||||
func removeFromFavorite(s *xorm.Session, entityID int64, a web.Auth, kind FavoriteKind) error {
|
||||
u, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
// Only error GetFromAuth is if it's a link share and we want to ignore that
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("entity_id = ? AND user_id = ? AND kind = ?", entityID, u.ID, kind).
|
||||
Delete(&Favorite{})
|
||||
return err
|
||||
}
|
||||
|
||||
func isFavorite(s *xorm.Session, entityID int64, a web.Auth, kind FavoriteKind) (is bool, err error) {
|
||||
u, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
// Only error GetFromAuth is if it's a link share and we want to ignore that
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return s.
|
||||
Where("entity_id = ? AND user_id = ? AND kind = ?", entityID, u.ID, kind).
|
||||
Exist(&Favorite{})
|
||||
}
|
||||
|
||||
func getFavorites(s *xorm.Session, entityIDs []int64, a web.Auth, kind FavoriteKind) (favorites map[int64]bool, err error) {
|
||||
favorites = make(map[int64]bool)
|
||||
u, err := user.GetFromAuth(a)
|
||||
if err != nil {
|
||||
// Only error GetFromAuth is if it's a link share and we want to ignore that
|
||||
return favorites, nil
|
||||
}
|
||||
|
||||
favs := []*Favorite{}
|
||||
err = s.Where(builder.And(
|
||||
builder.Eq{"user_id": u.ID},
|
||||
builder.Eq{"kind": kind},
|
||||
builder.In("entity_id", entityIDs),
|
||||
)).
|
||||
Find(&favs)
|
||||
|
||||
for _, fav := range favs {
|
||||
favorites[fav.EntityID] = true
|
||||
}
|
||||
return
|
||||
}
|
|
@ -41,6 +41,9 @@ type Bucket struct {
|
|||
// If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.
|
||||
IsDoneBucket bool `xorm:"BOOL" json:"is_done_bucket"`
|
||||
|
||||
// The position this bucket has when querying all buckets. See the tasks.position property on how to use this.
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
|
||||
// A timestamp when this bucket was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this bucket was last updated. You cannot change this value.
|
||||
|
@ -134,7 +137,10 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
|
||||
// Get all buckets for this list
|
||||
buckets := []*Bucket{}
|
||||
err = s.Where("list_id = ?", b.ListID).Find(&buckets)
|
||||
err = s.
|
||||
Where("list_id = ?", b.ListID).
|
||||
OrderBy("position").
|
||||
Find(&buckets)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -167,7 +173,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
opts.sortby = []*sortParam{
|
||||
{
|
||||
orderBy: orderAscending,
|
||||
sortBy: taskPropertyPosition,
|
||||
sortBy: taskPropertyKanbanPosition,
|
||||
},
|
||||
}
|
||||
opts.page = page
|
||||
|
@ -209,7 +215,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
|
|||
taskMap[t.ID] = t
|
||||
}
|
||||
|
||||
err = addMoreInfoToTasks(s, taskMap)
|
||||
err = addMoreInfoToTasks(s, taskMap, auth)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
@ -251,6 +257,12 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
|
|||
b.CreatedByID = b.CreatedBy.ID
|
||||
|
||||
_, err = s.Insert(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.Position = calculateDefaultPosition(b.ID, b.Position)
|
||||
_, err = s.Where("id = ?", b.ID).Update(b)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -275,7 +287,7 @@ func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
if doneBucket != nil && doneBucket.IsDoneBucket && b.IsDoneBucket {
|
||||
if doneBucket != nil && doneBucket.IsDoneBucket && b.IsDoneBucket && doneBucket.ID != b.ID {
|
||||
return &ErrOnlyOneDoneBucketPerList{
|
||||
BucketID: b.ID,
|
||||
ListID: b.ListID,
|
||||
|
@ -289,6 +301,7 @@ func (b *Bucket) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
"title",
|
||||
"limit",
|
||||
"is_done_bucket",
|
||||
"position",
|
||||
).
|
||||
Update(b)
|
||||
return
|
||||
|
|
|
@ -49,21 +49,23 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
assert.Len(t, buckets, 3)
|
||||
|
||||
// Assert all tasks are in the right bucket
|
||||
assert.Len(t, buckets[0].Tasks, 12)
|
||||
assert.Len(t, buckets[1].Tasks, 3)
|
||||
assert.Len(t, buckets[0].Tasks, 3)
|
||||
assert.Len(t, buckets[1].Tasks, 12)
|
||||
assert.Len(t, buckets[2].Tasks, 3)
|
||||
|
||||
// Assert we have bucket 0, 1, 2, 3 but not 4 (that belongs to a different list)
|
||||
assert.Equal(t, int64(1), buckets[0].ID)
|
||||
assert.Equal(t, int64(2), buckets[1].ID)
|
||||
// Assert we have bucket 1, 2, 3 but not 4 (that belongs to a different list) and their position
|
||||
assert.Equal(t, int64(2), buckets[0].ID)
|
||||
assert.Equal(t, int64(1), buckets[1].ID)
|
||||
assert.Equal(t, int64(3), buckets[2].ID)
|
||||
|
||||
// Kinda assert all tasks are in the right buckets
|
||||
assert.Equal(t, int64(1), buckets[0].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[0].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[2].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[1].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[1].Tasks[1].BucketID)
|
||||
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[2].BucketID)
|
||||
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[2].BucketID)
|
||||
|
@ -87,7 +89,8 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
|
||||
buckets := bucketsInterface.([]*Bucket)
|
||||
assert.Len(t, buckets, 3)
|
||||
assert.Equal(t, int64(2), buckets[0].Tasks[0].ID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[0].ID)
|
||||
assert.Equal(t, int64(33), buckets[1].Tasks[1].ID)
|
||||
})
|
||||
t.Run("accessed by link share", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
|
|
@ -61,7 +61,7 @@ func (Label) TableName() string {
|
|||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param label body models.Label true "The label object"
|
||||
// @Success 200 {object} models.Label "The created label object."
|
||||
// @Success 201 {object} models.Label "The created label object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid label object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /labels [put]
|
||||
|
|
|
@ -21,9 +21,12 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
@ -75,7 +78,7 @@ func (lt *LabelTask) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param task path int true "Task ID"
|
||||
// @Param label body models.LabelTask true "The label object"
|
||||
// @Success 200 {object} models.LabelTask "The created label relation object."
|
||||
// @Success 201 {object} models.LabelTask "The created label relation object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid label object provided."
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to add the label."
|
||||
// @Failure 404 {object} web.HTTPError "The label does not exist."
|
||||
|
@ -200,7 +203,7 @@ func getLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*lab
|
|||
if len(ids) > 0 {
|
||||
cond = builder.And(cond, builder.In("labels.id", ids))
|
||||
} else {
|
||||
cond = builder.And(cond, &builder.Like{"labels.title", "%" + opts.Search + "%"})
|
||||
cond = builder.And(cond, db.ILIKE("labels.title", opts.Search))
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.Page, opts.PerPage)
|
||||
|
@ -369,7 +372,7 @@ type LabelTaskBulk struct {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param label body models.LabelTaskBulk true "The array of labels"
|
||||
// @Param taskID path int true "Task ID"
|
||||
// @Success 200 {object} models.LabelTaskBulk "The updated labels object."
|
||||
// @Success 201 {object} models.LabelTaskBulk "The updated labels object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid label object provided."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/labels/bulk [post]
|
||||
|
|
|
@ -30,6 +30,24 @@ import (
|
|||
)
|
||||
|
||||
func TestLabelTask_ReadAll(t *testing.T) {
|
||||
label := Label{
|
||||
ID: 4,
|
||||
Title: "Label #4 - visible via other task",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
CreatedByID: 2,
|
||||
CreatedBy: &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
ID int64
|
||||
TaskID int64
|
||||
|
@ -62,23 +80,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
wantLabels: []*labelWithTaskID{
|
||||
{
|
||||
TaskID: 1,
|
||||
Label: Label{
|
||||
ID: 4,
|
||||
Title: "Label #4 - visible via other task",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
CreatedByID: 2,
|
||||
CreatedBy: &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
},
|
||||
Label: label,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -104,6 +106,22 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
wantErr: true,
|
||||
errType: IsErrTaskDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "search",
|
||||
fields: fields{
|
||||
TaskID: 1,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 1},
|
||||
search: "VISIBLE",
|
||||
},
|
||||
wantLabels: []*labelWithTaskID{
|
||||
{
|
||||
TaskID: 1,
|
||||
Label: label,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -51,7 +51,6 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
|
@ -166,7 +165,6 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
|
|
|
@ -20,12 +20,15 @@ import (
|
|||
"errors"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
|
@ -120,7 +123,7 @@ func (share *LinkSharing) toUser() *user.User {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param list path int true "List ID"
|
||||
// @Param label body models.LinkSharing true "The new link share object"
|
||||
// @Success 200 {object} models.LinkSharing "The created link share object."
|
||||
// @Success 201 {object} models.LinkSharing "The created link share object."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid link share object provided."
|
||||
// @Failure 403 {object} web.HTTPError "Not allowed to add the list share."
|
||||
// @Failure 404 {object} web.HTTPError "The list does not exist."
|
||||
|
@ -206,7 +209,14 @@ func (share *LinkSharing) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
|||
|
||||
var shares []*LinkSharing
|
||||
query := s.
|
||||
Where("list_id = ? AND hash LIKE ?", share.ListID, "%"+search+"%")
|
||||
Where(builder.And(
|
||||
builder.Eq{"list_id": share.ListID},
|
||||
builder.Or(
|
||||
db.ILIKE("hash", search),
|
||||
db.ILIKE("name", search),
|
||||
),
|
||||
))
|
||||
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
|
|
|
@ -103,6 +103,21 @@ func TestLinkSharing_ReadAll(t *testing.T) {
|
|||
assert.Empty(t, sharing.Password)
|
||||
}
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
share := &LinkSharing{
|
||||
ListID: 1,
|
||||
}
|
||||
all, _, _, err := share.ReadAll(s, doer, "wITHPASS", 1, -1)
|
||||
shares := all.([]*LinkSharing)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, shares, 1)
|
||||
assert.Equal(t, int64(4), shares[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLinkSharing_ReadOne(t *testing.T) {
|
||||
|
|
|
@ -21,6 +21,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
|
@ -49,12 +51,6 @@ type List struct {
|
|||
|
||||
// The user who created this list.
|
||||
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
|
||||
// An array of tasks which belong to the list.
|
||||
// Deprecated: you should use the dedicated task list endpoint because it has support for pagination and filtering
|
||||
Tasks []*Task `xorm:"-" json:"-"`
|
||||
|
||||
// Only used for migration.
|
||||
Buckets []*Bucket `xorm:"-" json:"-"`
|
||||
|
||||
// Whether or not a list is archived.
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
|
@ -64,13 +60,16 @@ type List struct {
|
|||
// 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"`
|
||||
|
||||
// True if a list is a favorite. Favorite lists show up in a separate namespace.
|
||||
IsFavorite bool `xorm:"default false" json:"is_favorite"`
|
||||
// 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"`
|
||||
|
||||
// The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it.
|
||||
// Will only returned when retreiving one list.
|
||||
Subscription *Subscription `xorm:"-" json:"subscription,omitempty"`
|
||||
|
||||
// The position this list has when querying all lists. See the tasks.position property on how to use this.
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
|
||||
// A timestamp when this list was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this list was last updated. You cannot change this value.
|
||||
|
@ -80,6 +79,15 @@ type List struct {
|
|||
web.Rights `xorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
type ListWithTasksAndBuckets struct {
|
||||
List
|
||||
// An array of tasks which belong to the list.
|
||||
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
|
||||
// Only used for migration.
|
||||
Buckets []*Bucket `xorm:"-" json:"buckets"`
|
||||
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
|
||||
}
|
||||
|
||||
// TableName returns a better name for the lists table
|
||||
func (l *List) TableName() string {
|
||||
return "lists"
|
||||
|
@ -155,7 +163,7 @@ func GetListsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (lists [
|
|||
}
|
||||
|
||||
// get more list details
|
||||
err = addListDetails(s, lists)
|
||||
err = addListDetails(s, lists, doer)
|
||||
return lists, err
|
||||
}
|
||||
|
||||
|
@ -183,7 +191,7 @@ func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
lists := []*List{list}
|
||||
err = addListDetails(s, lists)
|
||||
err = addListDetails(s, lists, a)
|
||||
return lists, 0, 0, err
|
||||
}
|
||||
|
||||
|
@ -201,7 +209,7 @@ func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per
|
|||
}
|
||||
|
||||
// Add more list details
|
||||
err = addListDetails(s, lists)
|
||||
err = addListDetails(s, lists, a)
|
||||
return lists, resultCount, totalItems, err
|
||||
}
|
||||
|
||||
|
@ -266,6 +274,11 @@ func (l *List) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l.Subscription, err = GetSubscription(s, SubscriptionEntityList, l.ID, a)
|
||||
return
|
||||
}
|
||||
|
@ -279,7 +292,10 @@ func GetListSimpleByID(s *xorm.Session, listID int64) (list *List, err error) {
|
|||
return nil, ErrListDoesNotExist{ID: listID}
|
||||
}
|
||||
|
||||
exists, err := s.Where("id = ?", listID).Get(list)
|
||||
exists, err := s.
|
||||
Where("id = ?", listID).
|
||||
OrderBy("position").
|
||||
Get(list)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -356,6 +372,7 @@ func getUserListsStatement(userID int64) *builder.Builder {
|
|||
builder.Eq{"un.user_id": userID},
|
||||
builder.Eq{"l.owner_id": userID},
|
||||
)).
|
||||
OrderBy("position").
|
||||
GroupBy("l.id")
|
||||
}
|
||||
|
||||
|
@ -391,10 +408,9 @@ func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resu
|
|||
}
|
||||
}
|
||||
|
||||
filterCond = db.ILIKE("l.title", opts.search)
|
||||
if len(ids) > 0 {
|
||||
filterCond = builder.In("l.id", ids)
|
||||
} else {
|
||||
filterCond = &builder.Like{"l.title", "%" + opts.search + "%"}
|
||||
}
|
||||
|
||||
// Gets all Lists where the user is either owner or in a team which has access to the list
|
||||
|
@ -421,7 +437,7 @@ func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resu
|
|||
}
|
||||
|
||||
// addListDetails adds owner user objects and list tasks to all lists in the slice
|
||||
func addListDetails(s *xorm.Session, lists []*List) (err error) {
|
||||
func addListDetails(s *xorm.Session, lists []*List, a web.Auth) (err error) {
|
||||
if len(lists) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -441,7 +457,9 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) {
|
|||
}
|
||||
|
||||
var fileIDs []int64
|
||||
var listIDs []int64
|
||||
for _, l := range lists {
|
||||
listIDs = append(listIDs, l.ID)
|
||||
if o, exists := owners[l.OwnerID]; exists {
|
||||
l.Owner = o
|
||||
}
|
||||
|
@ -451,6 +469,19 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) {
|
|||
fileIDs = append(fileIDs, l.BackgroundFileID)
|
||||
}
|
||||
|
||||
favs, err := getFavorites(s, listIDs, a, FavoriteKindList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 len(fileIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
@ -536,6 +567,20 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
|
|||
|
||||
if list.ID == 0 {
|
||||
_, 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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We need to specify the cols we want to update here to be able to un-archive lists
|
||||
colsToUpdate := []string{
|
||||
|
@ -543,17 +588,36 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
|
|||
"is_archived",
|
||||
"identifier",
|
||||
"hex_color",
|
||||
"is_favorite",
|
||||
"background_file_id",
|
||||
"position",
|
||||
}
|
||||
if list.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -568,7 +632,6 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
|
|||
*list = *l
|
||||
err = list.ReadOne(s, auth)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Update implements the update method of CRUDable
|
||||
|
@ -593,9 +656,9 @@ func (l *List) Update(s *xorm.Session, a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
f.IsFavorite = l.IsFavorite
|
||||
f.Title = l.Title
|
||||
f.Description = l.Description
|
||||
f.IsFavorite = l.IsFavorite
|
||||
err = f.Update(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -640,7 +703,7 @@ func updateListByTaskID(s *xorm.Session, taskID int64) (err error) {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param namespaceID path int true "Namespace ID"
|
||||
// @Param list body models.List true "The list you want to create."
|
||||
// @Success 200 {object} models.List "The created list."
|
||||
// @Success 201 {object} models.List "The created list."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
@ -702,11 +765,19 @@ func (l *List) Delete(s *xorm.Session, a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
// Delete all tasks on that list
|
||||
_, err = s.Where("list_id = ?", l.ID).Delete(&Task{})
|
||||
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
|
||||
tasks, _, _, err := getRawTasksForLists(s, []*List{l}, a, &taskOptions{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
err = task.Delete(s, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return events.Dispatch(&ListDeletedEvent{
|
||||
List: l,
|
||||
Doer: a,
|
||||
|
|
|
@ -61,7 +61,7 @@ func (ld *ListDuplicate) CanCreate(s *xorm.Session, a web.Auth) (canCreate bool,
|
|||
// @Security JWTKeyAuth
|
||||
// @Param listID path int true "The list ID to duplicate"
|
||||
// @Param list body models.ListDuplicate true "The target namespace which should hold the copied list."
|
||||
// @Success 200 {object} models.ListDuplicate "The created list."
|
||||
// @Success 201 {object} models.ListDuplicate "The created list."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid list duplicate object provided."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list or namespace"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
@ -107,12 +107,111 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
|
||||
log.Debugf("Duplicated all buckets from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
err = duplicateTasks(s, doer, ld, bucketMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Background files + unsplash info
|
||||
if ld.List.BackgroundFileID != 0 {
|
||||
|
||||
log.Debugf("Duplicating background %d from list %d into %d", ld.List.BackgroundFileID, ld.ListID, ld.List.ID)
|
||||
|
||||
f := &files.File{ID: ld.List.BackgroundFileID}
|
||||
if err := f.LoadFileMetaByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.LoadFileByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.File.Close()
|
||||
|
||||
file, err := files.Create(f.File, f.Name, f.Size, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get unsplash info if applicable
|
||||
up, err := GetUnsplashPhotoByFileID(s, ld.List.BackgroundFileID)
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return err
|
||||
}
|
||||
if up != nil {
|
||||
up.ID = 0
|
||||
up.FileID = file.ID
|
||||
if err := up.Save(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := SetListBackground(s, ld.List.ID, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated list background from list %d into %d", ld.ListID, ld.List.ID)
|
||||
}
|
||||
|
||||
// Rights / Shares
|
||||
// To keep it simple(r) we will only copy rights which are directly used with the list, no namespace changes.
|
||||
users := []*ListUser{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range users {
|
||||
u.ID = 0
|
||||
u.ListID = ld.List.ID
|
||||
if _, err := s.Insert(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated user shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
teams := []*TeamList{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&teams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, t := range teams {
|
||||
t.ID = 0
|
||||
t.ListID = ld.List.ID
|
||||
if _, err := s.Insert(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new link shares if any are available
|
||||
linkShares := []*LinkSharing{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&linkShares)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, share := range linkShares {
|
||||
share.ID = 0
|
||||
share.ListID = ld.List.ID
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
if _, err := s.Insert(share); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all link shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ListDuplicate, bucketMap map[int64]int64) (err error) {
|
||||
// Get all tasks + all task details
|
||||
tasks, _, _, err := getTasksForLists(s, []*List{{ID: ld.ListID}}, doer, &taskOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// This map contains the old task id as key and the new duplicated task id as value.
|
||||
// It is used to map old task items to new ones.
|
||||
taskMap := make(map[int64]int64)
|
||||
|
@ -255,91 +354,5 @@ func (ld *ListDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
|
|||
|
||||
log.Debugf("Duplicated all task relations from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
// Background files + unsplash info
|
||||
if ld.List.BackgroundFileID != 0 {
|
||||
|
||||
log.Debugf("Duplicating background %d from list %d into %d", ld.List.BackgroundFileID, ld.ListID, ld.List.ID)
|
||||
|
||||
f := &files.File{ID: ld.List.BackgroundFileID}
|
||||
if err := f.LoadFileMetaByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.LoadFileByID(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.File.Close()
|
||||
|
||||
file, err := files.Create(f.File, f.Name, f.Size, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get unsplash info if applicable
|
||||
up, err := GetUnsplashPhotoByFileID(s, ld.List.BackgroundFileID)
|
||||
if err != nil && files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return err
|
||||
}
|
||||
if up != nil {
|
||||
up.ID = 0
|
||||
up.FileID = file.ID
|
||||
if err := up.Save(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := SetListBackground(s, ld.List.ID, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated list background from list %d into %d", ld.ListID, ld.List.ID)
|
||||
}
|
||||
|
||||
// Rights / Shares
|
||||
// To keep it simple(r) we will only copy rights which are directly used with the list, no namespace changes.
|
||||
users := []*ListUser{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range users {
|
||||
u.ID = 0
|
||||
u.ListID = ld.List.ID
|
||||
if _, err := s.Insert(u); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated user shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
teams := []*TeamList{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&teams)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, t := range teams {
|
||||
t.ID = 0
|
||||
t.ListID = ld.List.ID
|
||||
if _, err := s.Insert(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new link shares if any are available
|
||||
linkShares := []*LinkSharing{}
|
||||
err = s.Where("list_id = ?", ld.ListID).Find(&linkShares)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, share := range linkShares {
|
||||
share.ID = 0
|
||||
share.ListID = ld.List.ID
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
if _, err := s.Insert(share); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("Duplicated all link shares from list %d into %d", ld.ListID, ld.List.ID)
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ package models
|
|||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
|
@ -65,7 +67,7 @@ type TeamWithRight struct {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body models.TeamList true "The team you want to add to the list."
|
||||
// @Success 200 {object} models.TeamList "The created team<->list relation."
|
||||
// @Success 201 {object} models.TeamList "The created team<->list relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid team list object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The team does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
|
@ -198,7 +200,7 @@ func (tl *TeamList) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
Table("teams").
|
||||
Join("INNER", "team_lists", "team_id = teams.id").
|
||||
Where("team_lists.list_id = ?", tl.ListID).
|
||||
Where("teams.name LIKE ?", "%"+search+"%")
|
||||
Where(db.ILIKE("teams.name", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
|
|
|
@ -81,6 +81,20 @@ func TestTeamList_ReadAll(t *testing.T) {
|
|||
assert.True(t, IsErrNeedToHaveListReadAccess(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
tl := TeamList{
|
||||
ListID: 19,
|
||||
}
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
teams, _, _, err := tl.ReadAll(s, u, "TEAM9", 1, 50)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(teams).Kind(), reflect.Slice)
|
||||
ts := teams.([]*TeamWithRight)
|
||||
assert.Len(t, ts, 1)
|
||||
assert.Equal(t, int64(9), ts[0].ID)
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestTeamList_Create(t *testing.T) {
|
||||
|
|
|
@ -200,8 +200,11 @@ func TestList_ReadAll(t *testing.T) {
|
|||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice)
|
||||
ls := reflect.ValueOf(lists3)
|
||||
assert.Equal(t, 16, ls.Len())
|
||||
ls := lists3.([]*List)
|
||||
assert.Equal(t, 16, len(ls))
|
||||
assert.Equal(t, int64(3), ls[0].ID) // List 3 has a position of 1 and should be sorted first
|
||||
assert.Equal(t, int64(1), ls[1].ID)
|
||||
assert.Equal(t, int64(4), ls[2].ID)
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("lists for nonexistant user", func(t *testing.T) {
|
||||
|
@ -214,6 +217,19 @@ func TestList_ReadAll(t *testing.T) {
|
|||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
_ = s.Close()
|
||||
})
|
||||
t.Run("search", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
u := &user.User{ID: 1}
|
||||
list := List{}
|
||||
lists3, _, _, err := list.ReadAll(s, u, "TEST10", 1, 50)
|
||||
|
||||
assert.NoError(t, err)
|
||||
ls := lists3.([]*List)
|
||||
assert.Equal(t, 1, len(ls))
|
||||
assert.Equal(t, int64(10), ls[0].ID)
|
||||
_ = s.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestList_ReadOne(t *testing.T) {
|
||||
|
|
|
@ -19,6 +19,8 @@ package models
|
|||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
@ -68,7 +70,7 @@ type UserWithRight struct {
|
|||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body models.ListUser true "The user you want to add to the list."
|
||||
// @Success 200 {object} models.ListUser "The created user<->list relation."
|
||||
// @Success 201 {object} models.ListUser "The created user<->list relation."
|
||||
// @Failure 400 {object} web.HTTPError "Invalid user list object provided."
|
||||
// @Failure 404 {object} web.HTTPError "The user does not exist."
|
||||
// @Failure 403 {object} web.HTTPError "The user does not have access to the list"
|
||||
|
@ -204,7 +206,7 @@ func (lu *ListUser) ReadAll(s *xorm.Session, a web.Auth, search string, page int
|
|||
query := s.
|
||||
Join("INNER", "users_lists", "user_id = users.id").
|
||||
Where("users_lists.list_id = ?", lu.ListID).
|
||||
Where("users.username LIKE ?", "%"+search+"%")
|
||||
Where(db.ILIKE("users.username", search))
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
}
|
||||
|
|
|
@ -143,6 +143,33 @@ func TestListUser_Create(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestListUser_ReadAll(t *testing.T) {
|
||||
user1Read := &UserWithRight{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
}
|
||||
user2Read := &UserWithRight{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
|
@ -175,33 +202,8 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
a: &user.User{ID: 3},
|
||||
},
|
||||
want: []*UserWithRight{
|
||||
{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
user1Read,
|
||||
user2Read,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -215,6 +217,19 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
wantErr: true,
|
||||
errType: IsErrNeedToHaveListReadAccess,
|
||||
},
|
||||
{
|
||||
name: "Search",
|
||||
fields: fields{
|
||||
ListID: 3,
|
||||
},
|
||||
args: args{
|
||||
a: &user.User{ID: 3},
|
||||
search: "USER2",
|
||||
},
|
||||
want: []*UserWithRight{
|
||||
user2Read,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -25,7 +25,10 @@ import (
|
|||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
"code.vikunja.io/api/pkg/notifications"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// RegisterListeners registers all event listeners
|
||||
|
@ -44,6 +47,10 @@ func RegisterListeners() {
|
|||
events.RegisterListener((&ListCreatedEvent{}).Name(), &SendListCreatedNotification{})
|
||||
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SubscribeAssigneeToTask{})
|
||||
events.RegisterListener((&TeamMemberAddedEvent{}).Name(), &SendTeamMemberAddedNotification{})
|
||||
events.RegisterListener((&TaskCommentUpdatedEvent{}).Name(), &HandleTaskCommentEditMentions{})
|
||||
events.RegisterListener((&TaskCreatedEvent{}).Name(), &HandleTaskCreateMentions{})
|
||||
events.RegisterListener((&TaskUpdatedEvent{}).Name(), &HandleTaskUpdatedMentions{})
|
||||
events.RegisterListener((&UserDataExportRequestedEvent{}).Name(), &HandleUserDataExport{})
|
||||
}
|
||||
|
||||
//////
|
||||
|
@ -58,7 +65,7 @@ func (s *IncreaseTaskCounter) Name() string {
|
|||
return "task.counter.increase"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event IncreaseTaskCounter listens on is fired
|
||||
// Handle is executed when the event IncreaseTaskCounter listens on is fired
|
||||
func (s *IncreaseTaskCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.IncrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
|
@ -72,11 +79,56 @@ func (s *DecreaseTaskCounter) Name() string {
|
|||
return "task.counter.decrease"
|
||||
}
|
||||
|
||||
// Hanlde is executed when the event DecreaseTaskCounter listens on is fired
|
||||
// Handle is executed when the event DecreaseTaskCounter listens on is fired
|
||||
func (s *DecreaseTaskCounter) Handle(msg *message.Message) (err error) {
|
||||
return keyvalue.DecrBy(metrics.TaskCountKey, 1)
|
||||
}
|
||||
|
||||
func notifyMentionedUsers(sess *xorm.Session, task *Task, text string, n notifications.NotificationWithSubject) (users map[int64]*user.User, err error) {
|
||||
users, err = FindMentionedUsersInText(sess, text)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Processing %d mentioned users for text %d", len(users), n.SubjectID())
|
||||
|
||||
var notified int
|
||||
for _, u := range users {
|
||||
can, _, err := task.CanRead(sess, u)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
||||
if !can {
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't notify a user if they were already notified
|
||||
dbn, err := notifications.GetNotificationsForNameAndUser(sess, u.ID, n.Name(), n.SubjectID())
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
|
||||
if len(dbn) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
err = notifications.Notify(u, n)
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
notified++
|
||||
}
|
||||
|
||||
log.Debugf("Notified %d mentioned users for text %d", notified, n.SubjectID())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SendTaskCommentNotification represents a listener
|
||||
type SendTaskCommentNotification struct {
|
||||
}
|
||||
|
@ -97,6 +149,17 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) {
|
|||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
n := &TaskCommentNotification{
|
||||
Doer: event.Doer,
|
||||
Task: event.Task,
|
||||
Comment: event.Comment,
|
||||
Mentioned: true,
|
||||
}
|
||||
mentionedUsers, err := notifyMentionedUsers(sess, event.Task, event.Comment.Comment, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -109,6 +172,10 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) {
|
|||
continue
|
||||
}
|
||||
|
||||
if _, has := mentionedUsers[subscriber.UserID]; has {
|
||||
continue
|
||||
}
|
||||
|
||||
n := &TaskCommentNotification{
|
||||
Doer: event.Doer,
|
||||
Task: event.Task,
|
||||
|
@ -123,6 +190,36 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// HandleTaskCommentEditMentions represents a listener
|
||||
type HandleTaskCommentEditMentions struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the HandleTaskCommentEditMentions listener
|
||||
func (s *HandleTaskCommentEditMentions) Name() string {
|
||||
return "handle.task.comment.edit.mentions"
|
||||
}
|
||||
|
||||
// Handle is executed when the event HandleTaskCommentEditMentions listens on is fired
|
||||
func (s *HandleTaskCommentEditMentions) Handle(msg *message.Message) (err error) {
|
||||
event := &TaskCommentUpdatedEvent{}
|
||||
err = json.Unmarshal(msg.Payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
n := &TaskCommentNotification{
|
||||
Doer: event.Doer,
|
||||
Task: event.Task,
|
||||
Comment: event.Comment,
|
||||
Mentioned: true,
|
||||
}
|
||||
_, err = notifyMentionedUsers(sess, event.Task, event.Comment.Comment, n)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendTaskAssignedNotification represents a listener
|
||||
type SendTaskAssignedNotification struct {
|
||||
}
|
||||
|
@ -247,6 +344,65 @@ func (s *SubscribeAssigneeToTask) Handle(msg *message.Message) (err error) {
|
|||
return sess.Commit()
|
||||
}
|
||||
|
||||
// HandleTaskCreateMentions represents a listener
|
||||
type HandleTaskCreateMentions struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the HandleTaskCreateMentions listener
|
||||
func (s *HandleTaskCreateMentions) Name() string {
|
||||
return "task.created.mentions"
|
||||
}
|
||||
|
||||
// Handle is executed when the event HandleTaskCreateMentions listens on is fired
|
||||
func (s *HandleTaskCreateMentions) Handle(msg *message.Message) (err error) {
|
||||
event := &TaskCreatedEvent{}
|
||||
err = json.Unmarshal(msg.Payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
n := &UserMentionedInTaskNotification{
|
||||
Task: event.Task,
|
||||
Doer: event.Doer,
|
||||
IsNew: true,
|
||||
}
|
||||
_, err = notifyMentionedUsers(sess, event.Task, event.Task.Description, n)
|
||||
return err
|
||||
}
|
||||
|
||||
// HandleTaskUpdatedMentions represents a listener
|
||||
type HandleTaskUpdatedMentions struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the HandleTaskUpdatedMentions listener
|
||||
func (s *HandleTaskUpdatedMentions) Name() string {
|
||||
return "task.updated.mentions"
|
||||
}
|
||||
|
||||
// Handle is executed when the event HandleTaskUpdatedMentions listens on is fired
|
||||
func (s *HandleTaskUpdatedMentions) Handle(msg *message.Message) (err error) {
|
||||
event := &TaskUpdatedEvent{}
|
||||
err = json.Unmarshal(msg.Payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
n := &UserMentionedInTaskNotification{
|
||||
Task: event.Task,
|
||||
Doer: event.Doer,
|
||||
IsNew: false,
|
||||
}
|
||||
_, err = notifyMentionedUsers(sess, event.Task, event.Task.Description, n)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
///////
|
||||
// List Event Listeners
|
||||
|
||||
|
@ -396,9 +552,52 @@ func (s *SendTeamMemberAddedNotification) Handle(msg *message.Message) (err erro
|
|||
return err
|
||||
}
|
||||
|
||||
// Don't notify the user themselves
|
||||
if event.Doer.ID == event.Member.ID {
|
||||
return nil
|
||||
}
|
||||
|
||||
return notifications.Notify(event.Member, &TeamMemberAddedNotification{
|
||||
Member: event.Member,
|
||||
Doer: event.Doer,
|
||||
Team: event.Team,
|
||||
})
|
||||
}
|
||||
|
||||
// HandleUserDataExport represents a listener
|
||||
type HandleUserDataExport struct {
|
||||
}
|
||||
|
||||
// Name defines the name for the HandleUserDataExport listener
|
||||
func (s *HandleUserDataExport) Name() string {
|
||||
return "handle.user.data.export"
|
||||
}
|
||||
|
||||
// Handle is executed when the event HandleUserDataExport listens on is fired
|
||||
func (s *HandleUserDataExport) Handle(msg *message.Message) (err error) {
|
||||
event := &UserDataExportRequestedEvent{}
|
||||
err = json.Unmarshal(msg.Payload, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Starting to export user data for user %d...", event.User.ID)
|
||||
|
||||
sess := db.NewSession()
|
||||
defer sess.Close()
|
||||
err = sess.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = ExportUserData(sess, event.User)
|
||||
if err != nil {
|
||||
_ = sess.Rollback()
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Done exporting user data for user %d...", event.User.ID)
|
||||
|
||||
err = sess.Commit()
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -22,9 +22,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue