forked from vikunja/vikunja
Compare commits
66 Commits
renovate/g
...
master
Author | SHA1 | Date |
---|---|---|
renovate | 9c4c265767 | |
kolaente | a525787ab7 | |
kolaente | aae1bc3cab | |
kolaente | 42ddee8d6f | |
renovate | 54b18b3c59 | |
kolaente | 5a04f1ecf4 | |
kolaente | a0fb8bd32d | |
konrad | c685250c96 | |
renovate | 9ea1104764 | |
kolaente | 2ef2c7523d | |
konrad | e5e30d0915 | |
kolaente | c37b776f7a | |
kolaente | e9bc3246ce | |
konrad | e89e6d47d4 | |
kolaente | 292c815000 | |
kolaente | f0b1de5ec6 | |
kolaente | 4204af255c | |
renovate | b0948a37d4 | |
kolaente | cc47d11792 | |
kolaente | b6b82e6f54 | |
kolaente | 16ed5fd982 | |
kolaente | 24aa68f090 | |
kolaente | 98ef052967 | |
kolaente | 8a01b20e1e | |
kolaente | 5a86f44fc3 | |
konrad | fe43173b6c | |
konrad | 587ce92dc9 | |
konrad | 8758fb6ac5 | |
kolaente | 92e5e2db6a | |
renovate | 851f0d6c08 | |
kolaente | 333444b4e1 | |
konrad | 28a5c1f7b2 | |
renovate | 4533ac6b28 | |
kolaente | 91a3b7aba2 | |
kolaente | 4137d4aed2 | |
kolaente | cb095d70df | |
kolaente | e43e601912 | |
kolaente | 55cd40d175 | |
renovate | 55cd74efca | |
renovate | a9d0079bf3 | |
konrad | 8ac158cdb4 | |
renovate | def2362682 | |
kolaente | 18f6e31b54 | |
kolaente | 7e1d0a81bf | |
renovate | f30e405229 | |
kolaente | 563fe16bd4 | |
kolaente | a30a358b73 | |
kolaente | 497499e221 | |
kolaente | 1fdc51078e | |
renovate | 60f343a926 | |
konrad | fd3ccd6130 | |
kolaente | a97ce76f1f | |
kolaente | 83cf6e191c | |
kolaente | f0ac6e89be | |
renovate | 23950c0602 | |
renovate | 19a05a7c2c | |
renovate | 299e856736 | |
renovate | 6d95dc16c2 | |
kolaente | fa28ddc2f7 | |
kolaente | 56dbb564ea | |
renovate | 711124f5c0 | |
kolaente | 5606f987fa | |
kolaente | f0cb3b5e36 | |
konrad | 231dc3913f | |
renovate | 2b1fc441e6 | |
konrad | e433289832 |
24
.drone1.yml
24
.drone1.yml
|
@ -67,8 +67,9 @@ steps:
|
|||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
commands:
|
||||
- make generate
|
||||
- make test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
@ -79,8 +80,9 @@ steps:
|
|||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
commands:
|
||||
- make generate
|
||||
- make test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
@ -95,8 +97,9 @@ steps:
|
|||
VIKUNJA_DATABASE_PASSWORD: vikunjatest
|
||||
VIKUNJA_DATABASE_DATABASE: vikunjatest
|
||||
commands:
|
||||
- make generate
|
||||
- make test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
@ -112,8 +115,9 @@ steps:
|
|||
VIKUNJA_DATABASE_DATABASE: vikunjatest
|
||||
VIKUNJA_DATABASE_SSLMODE: disable
|
||||
commands:
|
||||
- make generate
|
||||
- make test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
@ -121,8 +125,9 @@ steps:
|
|||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
commands:
|
||||
- make generate
|
||||
- make integration-test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
@ -133,8 +138,9 @@ steps:
|
|||
VIKUNJA_TESTS_USE_CONFIG: 1
|
||||
VIKUNJA_DATABASE_TYPE: sqlite
|
||||
commands:
|
||||
- make generate
|
||||
- make integration-test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
@ -149,8 +155,9 @@ steps:
|
|||
VIKUNJA_DATABASE_PASSWORD: vikunjatest
|
||||
VIKUNJA_DATABASE_DATABASE: vikunjatest
|
||||
commands:
|
||||
- make generate
|
||||
- make integration-test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
@ -166,8 +173,9 @@ steps:
|
|||
VIKUNJA_DATABASE_DATABASE: vikunjatest
|
||||
VIKUNJA_DATABASE_SSLMODE: disable
|
||||
commands:
|
||||
- make generate
|
||||
- make integration-test
|
||||
depends_on: [ build ]
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
|
|
99
CHANGELOG.md
99
CHANGELOG.md
|
@ -7,6 +7,105 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
All releases can be found on https://code.vikunja.io/api/releases.
|
||||
|
||||
## [0.13.1] - 2020-05-19
|
||||
|
||||
### Fixed
|
||||
|
||||
* Don't get all tasks if a user has no lists
|
||||
|
||||
## [0.13] - 2020-05-12
|
||||
|
||||
#### Added
|
||||
|
||||
* Add 2fa for authentification (#383)
|
||||
* Add categories to error docs
|
||||
* Add changing email for users
|
||||
* Add community link
|
||||
* Add configuration options for log level
|
||||
* Add creating a new first bucket when creating a new list
|
||||
* Add docs for changing frontend url
|
||||
* Add endpoint to disable totp auth
|
||||
* Add endpoint to get the current users totp status
|
||||
* Add explanation to docs about cors
|
||||
* Add github token for renovate (#164)
|
||||
* Add gosec static analysis
|
||||
* Add moving tasks between lists (#389)
|
||||
* Add real buckets for tasks which don't have one (#446)
|
||||
* Add traefik 2 example configuration
|
||||
* Configure Renovate (#159)
|
||||
* Kanban (#393)
|
||||
* Task filters (#243)
|
||||
* Task Position (#412)
|
||||
|
||||
#### Fixed
|
||||
|
||||
* Add checking and logging when trying to put a task into a nonexisting bucket
|
||||
* Fix bucket ID being reset with no need to do so
|
||||
* Fix creating new things with a link share auth
|
||||
* Fix dependencies
|
||||
* Fix gosec in drone
|
||||
* Fix link share creation & creating admin link shares without admin rights
|
||||
* Fix moving tasks back into the empty (ID: 0) bucket
|
||||
* Fix moving tasks in buckets
|
||||
* Fix not moving its bucket when moving a task between lists
|
||||
* Fix pagination count for task collection
|
||||
* Fix parsing array style comparators by query param
|
||||
* Fix reference to reverse proxies in docs
|
||||
* Fix removing the last bucket
|
||||
* Fix replace statements for tail
|
||||
* Fix team rights not updating for namespace rights
|
||||
* Fix tests after renaming json fields to snake_case
|
||||
* Fix total label count when getting all labels (#477)
|
||||
* Remove setting task bucket to 0
|
||||
* Task Filter Fixes (#495)
|
||||
|
||||
#### Changed
|
||||
|
||||
* Change all json fields to snake_case
|
||||
* Change totp secret datatype from varchar to text
|
||||
* Update alpine Docker tag to v3.11 (#160)
|
||||
* Update docs theme
|
||||
* Update github.com/c2h5oh/datasize commit hash to 28bbd47 (#212)
|
||||
* Update github.com/gordonklaus/ineffassign commit hash to 7953dde (#233)
|
||||
* Update github.com/jgautheron/goconst commit hash to cda7ea3 (#228)
|
||||
* Update github.com/shurcooL/httpfs commit hash to 8d4bc4b (#229)
|
||||
* Update golang.org/x/crypto commit hash to 056763e (#222)
|
||||
* Update golang.org/x/crypto commit hash to 06a226f (#504)
|
||||
* Update golang.org/x/crypto commit hash to 0848c95 (#371)
|
||||
* Update golang.org/x/crypto commit hash to 3c4aac8 (#419)
|
||||
* Update golang.org/x/crypto commit hash to 44a6062 (#429)
|
||||
* Update golang.org/x/crypto commit hash to 4b2356b (#475)
|
||||
* Update golang.org/x/crypto commit hash to 4bdfaf4 (#438)
|
||||
* Update golang.org/x/crypto commit hash to 729f1e8 (#458)
|
||||
* Update golang.org/x/crypto commit hash to a76a400 (#411)
|
||||
* Update golang.org/x/lint commit hash to 738671d (#223)
|
||||
* Update module go-redis/redis to v6.15.7 (#234)
|
||||
* Update module go-redis/redis to v6.15.7 (#290)
|
||||
* Update module go-redis/redis to v7 (#277)
|
||||
* Update module go-redis/redis to v7 (#309)
|
||||
* Update module go-testfixtures/testfixtures/v3 to v3.1.2 (#457)
|
||||
* Update module go-testfixtures/testfixtures/v3 to v3.2.0 (#505)
|
||||
* Update module imdario/mergo to v0.3.9 (#238)
|
||||
* Update module labstack/echo/v4 to v4.1.16 (#241)
|
||||
* Update module lib/pq to v1.4.0 (#428)
|
||||
* Update module lib/pq to v1.5.0 (#476)
|
||||
* Update module lib/pq to v1.5.1 (#485)
|
||||
* Update module lib/pq to v1.5.2 (#491)
|
||||
* Update module olekukonko/tablewriter to v0.0.4 (#240)
|
||||
* Update module prometheus/client_golang to v0.9.4 (#245)
|
||||
* Update module prometheus/client_golang to v1
|
||||
* Update module prometheus/client_golang to v1.6.0 (#463)
|
||||
* Update module spf13/cobra to v0.0.7 (#271)
|
||||
* Update module spf13/viper to v1.6.2 (#272)
|
||||
* Update module spf13/viper to v1.6.3 (#291)
|
||||
* Update module spf13/viper to v1.7.0 (#494)
|
||||
* Update module stretchr/testify to v1.5.1 (#274)
|
||||
* Update Renovate Configuration (#161)
|
||||
* Update src.techknowlogick.com/xgo commit hash to bb0faa3 (#279)
|
||||
* Update src.techknowlogick.com/xgo commit hash to c43d4c4 (#224)
|
||||
* Update xorm redis cacher to use the xorm logger instead of a special seperate one
|
||||
* Update xorm to v1 (#323)
|
||||
|
||||
## [0.12] - 2020-04-04
|
||||
|
||||
#### Added
|
||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -23,15 +23,22 @@ RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
|
|||
# The actual image
|
||||
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
|
||||
# because of this, the container would not start when I compiled the image without cgo.
|
||||
FROM alpine:3.11
|
||||
FROM alpine:3.12
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
|
||||
WORKDIR /app/vikunja/
|
||||
COPY --from=build-env /go/src/code.vikunja.io/api/vikunja .
|
||||
RUN adduser -S -D vikunja -h /app/vikunja -H \
|
||||
&& chown vikunja -R /app/vikunja
|
||||
ENV VIKUNJA_SERVICE_ROOTPATH=/app/vikunja/
|
||||
|
||||
# Dynamic permission changing stuff
|
||||
ENV PUID 1000
|
||||
ENV PGID 1000
|
||||
RUN apk --no-cache add shadow && \
|
||||
addgroup -g ${PGID} vikunja && \
|
||||
adduser -s /bin/sh -D -G vikunja -u ${PUID} vikunja -h /app/vikunja -H && \
|
||||
chown vikunja -R /app/vikunja
|
||||
COPY run.sh /run.sh
|
||||
|
||||
# Fix time zone settings not working
|
||||
RUN apk --no-cache add tzdata
|
||||
|
||||
|
@ -40,6 +47,5 @@ RUN mkdir /app/vikunja/files && \
|
|||
chown -R vikunja /app/vikunja/files
|
||||
VOLUME /app/vikunja/files
|
||||
|
||||
USER vikunja
|
||||
CMD ["/app/vikunja/vikunja"]
|
||||
CMD ["/run.sh"]
|
||||
EXPOSE 3456
|
||||
|
|
2
Makefile
2
Makefile
|
@ -219,7 +219,7 @@ gocyclo-check:
|
|||
go get -u github.com/fzipp/gocyclo; \
|
||||
go install $(GOFLAGS) github.com/fzipp/gocyclo; \
|
||||
fi
|
||||
for S in $(GOFILES); do gocyclo -over 24 $$S || exit 1; done;
|
||||
for S in $(GOFILES); do gocyclo -over 29 $$S || exit 1; done;
|
||||
|
||||
.PHONY: static-check
|
||||
static-check:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
[![Build Status](https://drone1.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone1.kolaente.de/vikunja/api)
|
||||
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.12-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.13.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/git.kolaente.de/vikunja/api)](https://goreportcard.com/report/git.kolaente.de/vikunja/api)
|
||||
|
|
|
@ -30,6 +30,8 @@ service:
|
|||
timezone: GMT
|
||||
# Whether task comments should be enabled or not
|
||||
enabletaskcomments: true
|
||||
# Whether totp is enabled. In most cases you want to leave that enabled.
|
||||
enabletotp: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
|
@ -74,6 +76,8 @@ redis:
|
|||
|
||||
cors:
|
||||
# Whether to enable or disable cors headers.
|
||||
# Note: If you want to put the frontend and the api on seperate domains or ports, you will need to enable this.
|
||||
# Otherwise the frontend won't be able to make requests to the api through the browser.
|
||||
enable: true
|
||||
# A list of origins which may access the api.
|
||||
origins:
|
||||
|
@ -154,6 +158,20 @@ migration:
|
|||
# with the code obtained from the wunderlist api.
|
||||
# Note that the vikunja frontend expects this to be /migrate/wunderlist
|
||||
redirecturl:
|
||||
todoist:
|
||||
# Wheter to enable the todoist migrator or not
|
||||
enable: false
|
||||
# The client id, required for making requests to the wunderlist api
|
||||
# You need to register your vikunja instance at https://developer.todoist.com/appconsole.html to get this
|
||||
clientid:
|
||||
# The client secret, also required for making requests to the todoist api
|
||||
clientsecret:
|
||||
# The url where clients are redirected after they authorized Vikunja to access their todoist items.
|
||||
# This needs to match the url you entered when registering your Vikunja instance at todoist.
|
||||
# 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:
|
||||
|
||||
avatar:
|
||||
# Switch between avatar providers. Possible values are gravatar and default.
|
||||
|
@ -162,3 +180,19 @@ avatar:
|
|||
provider: gravatar
|
||||
# When using gravatar, this is the duration in seconds until a cached gravatar user avatar expires
|
||||
gravatarexpiration: 3600
|
||||
|
||||
backgrounds:
|
||||
# Whether to enable backgrounds for lists at all.
|
||||
enabled: false
|
||||
providers:
|
||||
unsplash:
|
||||
# Whether to enable setting backgrounds from unsplash as list backgrounds
|
||||
enabled: false
|
||||
# You need to create an application for your installation at https://unsplash.com/oauth/applications/new
|
||||
# and set the access token below.
|
||||
accesstoken:
|
||||
# The unsplash application id is only used for pingback and required as per their api guidelines.
|
||||
# You can find the Application ID in the dashboard for your API application. It should be a numeric ID.
|
||||
# It will only show in the UI if your application has been approved for Enterprise usage, therefore if
|
||||
# you’re in Demo mode, you can also find the ID in the URL at the end: https://unsplash.com/oauth/applications/:application_id
|
||||
applicationid:
|
||||
|
|
|
@ -73,6 +73,8 @@ service:
|
|||
timezone: GMT
|
||||
# Whether task comments should be enabled or not
|
||||
enabletaskcomments: true
|
||||
# Whether totp is enabled. In most cases you want to leave that enabled.
|
||||
enabletotp: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
|
@ -117,6 +119,8 @@ redis:
|
|||
|
||||
cors:
|
||||
# Whether to enable or disable cors headers.
|
||||
# Note: If you want to put the frontend and the api on seperate domains or ports, you will need to enable this.
|
||||
# Otherwise the frontend won't be able to make requests to the api through the browser.
|
||||
enable: true
|
||||
# A list of origins which may access the api.
|
||||
origins:
|
||||
|
@ -197,6 +201,20 @@ migration:
|
|||
# with the code obtained from the wunderlist api.
|
||||
# Note that the vikunja frontend expects this to be /migrate/wunderlist
|
||||
redirecturl:
|
||||
todoist:
|
||||
# Wheter to enable the todoist migrator or not
|
||||
enable: false
|
||||
# The client id, required for making requests to the wunderlist api
|
||||
# You need to register your vikunja instance at https://developer.todoist.com/appconsole.html to get this
|
||||
clientid:
|
||||
# The client secret, also required for making requests to the todoist api
|
||||
clientsecret:
|
||||
# The url where clients are redirected after they authorized Vikunja to access their todoist items.
|
||||
# This needs to match the url you entered when registering your Vikunja instance at todoist.
|
||||
# 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:
|
||||
|
||||
avatar:
|
||||
# Switch between avatar providers. Possible values are gravatar and default.
|
||||
|
@ -205,4 +223,20 @@ avatar:
|
|||
provider: gravatar
|
||||
# When using gravatar, this is the duration in seconds until a cached gravatar user avatar expires
|
||||
gravatarexpiration: 3600
|
||||
|
||||
backgrounds:
|
||||
# Whether to enable backgrounds for lists at all.
|
||||
enabled: false
|
||||
providers:
|
||||
unsplash:
|
||||
# Whether to enable setting backgrounds from unsplash as list backgrounds
|
||||
enabled: false
|
||||
# You need to create an application for your installation at https://unsplash.com/oauth/applications/new
|
||||
# and set the access token below.
|
||||
accesstoken:
|
||||
# The unsplash application id is only used for pingback and required as per their api guidelines.
|
||||
# You can find the Application ID in the dashboard for your API application. It should be a numeric ID.
|
||||
# It will only show in the UI if your application has been approved for Enterprise usage, therefore if
|
||||
# you’re in Demo mode, you can also find the ID in the URL at the end: https://unsplash.com/oauth/applications/:application_id
|
||||
applicationid:
|
||||
{{< /highlight >}}
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
---
|
||||
date: "2020-05-24:00:00+02:00"
|
||||
title: "Docker Walkthrough"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "setup"
|
||||
---
|
||||
|
||||
# Setup with docker from start to finish
|
||||
|
||||
This tutorial assumes basic knowledge of docker.
|
||||
It is aimed at beginners and should get you up and running quickly.
|
||||
|
||||
We'll use [docker compose](https://docs.docker.com/compose/) to make handling the bunch of containers easier.
|
||||
|
||||
> If you have any issues setting up vikunja, please don't hesitate to reach out to us via [matrix](https://riot.im/app/#/room/!dCRiCiLaCCFVNlDnYs:matrix.org?via=matrix.org), the [community forum](https://community.vikunja.io/) or even [email](mailto:hello@vikunja.io).
|
||||
|
||||
## Preparations (optional)
|
||||
|
||||
Create a directory for the project where all data and the compose file will live in.
|
||||
|
||||
## Create all necessary files
|
||||
|
||||
Create a `docker-compose.yml` file with the following contents in your directory:
|
||||
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
api:
|
||||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: root
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
frontend:
|
||||
image: vikunja/frontend
|
||||
restart: unless-stopped
|
||||
proxy:
|
||||
image: nginx
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- api
|
||||
- frontend
|
||||
restart: unless-stopped
|
||||
{{< /highlight >}}
|
||||
|
||||
This defines four services, each with their own container:
|
||||
|
||||
* An api service which runs the vikunja api. Most of the core logic lives here.
|
||||
* The frontend which will make vikunja actually usable for most people.
|
||||
* A database container which will store all lists, tasks, etc. We're using mariadb here, but you're free to use mysql or postgres if you want.
|
||||
* A proxy service which makes the frontend and api available on the same port, redirecting all requests to `/api` to the api container.
|
||||
If you already have a proxy on your host, you may want to check out the [reverse proxy examples]() to use that.
|
||||
By default, it uses port 80 on the host.
|
||||
To change to something different, you'll need to change the `ports` section in the service definition.
|
||||
The number before the colon is the host port - This is where you can reach vikunja from the outside once all is up and running.
|
||||
|
||||
For the proxy service we'll need another bit of configuration.
|
||||
Create an `nginx.conf` in your directory (next to the `docker-compose.yml` file) and add the following contents to it:
|
||||
|
||||
{{< highlight conf >}}
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:3456;
|
||||
}
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
This is a simple proxy configuration which will forward all requests to `/api/` to the api container and everything else to the frontend.
|
||||
|
||||
**Note:** Even if you want to make your installation available under a different port, you don't need to change anything in this configuration.
|
||||
|
||||
## Run it
|
||||
|
||||
Run `sudo docker-compose up` in your directory and take a look at the output you get.
|
||||
When first started, Vikunja will set up the database and run all migrations etc.
|
||||
Once it is ready, you should see a message like this one in your console:
|
||||
|
||||
```
|
||||
api_1 | 2020-05-24T11:15:37.560386009Z: INFO ▶ cmd/func1 025 Vikunja version 0.13.1+19-e9bc3246ce, built at Sun, 24 May 2020 11:10:36 +0000
|
||||
api_1 | ⇨ http server started on [::]:3456
|
||||
```
|
||||
|
||||
This indicates all setup has been successful.
|
||||
If you get any errors, see below:
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Vikunja might not run on the first try.
|
||||
There are a few potential issues that could be causing this.
|
||||
|
||||
#### No connection to the database
|
||||
|
||||
Indicated by an error message like this one from the api container:
|
||||
|
||||
```
|
||||
2020/05/23 15:37:59 Config File "config" Not Found in "[/app/vikunja /etc/vikunja /app/vikunja/.config/vikunja]"
|
||||
2020/05/23 15:37:59 Using default config.
|
||||
2020-05-23T15:37:59.974435725Z: CRITICAL ▶ migration/Migrate 002 Migration failed: dial tcp 172.19.0.2:3306: connect: connection refused
|
||||
```
|
||||
|
||||
Especially when using mysql, this can happen on first start, because the mysql database container will take a few seconds to start.
|
||||
Vikunja does not know the container is not ready, therefore it will just try to connect to the db, fail since it is not ready and exit.
|
||||
|
||||
If you're using the docker compose example from above, you may notice the `restart: unless-stopped` option at the api service.
|
||||
This tells docker to restart the api container if it exits, unless you explicitly stop it.
|
||||
Therefore, it should "magically fix itself" by automatically restarting the container.
|
||||
|
||||
After a few seconds (or minutes) you should see a log message like this one from the mariadb container:
|
||||
|
||||
```
|
||||
2020-05-24 11:42:15 0 [Note] mysqld: ready for connections.
|
||||
Version: '10.4.12-MariaDB-1:10.4.12+maria~bionic' socket: '/var/run/mysqld/mysqld.sock' port: 3306 mariadb.org binary distribution
|
||||
```
|
||||
|
||||
The next restart of Vikunja should be successful.
|
||||
If not, there might be a different error or a bug with Vikunja, please reach out to us in that case.
|
||||
|
||||
(If you have an idea about how we could improve this, we'd like to hear it!)
|
||||
|
||||
#### "Not a directory"
|
||||
|
||||
If you get an error like this one:
|
||||
|
||||
```
|
||||
ERROR: for vikunja_proxy_1 Cannot start service proxy: OCI runtime create failed: container_linux.go:349: starting container process caused "process_linux.go:449: container init caused \"rootfs_linux.go:58: mounting \\\"vikunja/nginx.conf\\\" to rootfs \\\"/var/lib/docker/overlay2/9c8b8f9419c29dad0d1233fbb0a3c36cf403dabd7a55d6f0a47b0c1dd6029994/merged\\\" at \\\"/var/lib/docker/overlay2/9c8b8f9419c29dad0d1233fbb0a3c36cf403dabd7a55d6f0a47b0c1dd6029994/merged/etc/nginx/conf.d/default.conf\\\" caused \\\"not a directory\\\"\"": unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type
|
||||
```
|
||||
|
||||
this means docker tried to mount a directory from the host to a file in the container.
|
||||
This can happen if you did not create the `nginx.conf` file.
|
||||
Because there is a volume mount for it in the `docker-compose.yml`, Docker will create a folder because non exists, assuming you want to mount a folder into the container.
|
||||
|
||||
To fix this, create the file and restart the containers again.
|
||||
|
||||
#### Migration failed: commands out of sync
|
||||
|
||||
If you get an error like this one:
|
||||
|
||||
```
|
||||
2020/05/23 15:53:38 Config File "config" Not Found in "[/app/vikunja /etc/vikunja /app/vikunja/.config/vikunja]"
|
||||
2020/05/23 15:53:38 Using default config.
|
||||
2020-05-23T15:53:38.762747276Z: CRITICAL ▶ migration/Migrate 002 Migration failed: commands out of sync. Did you run multiple statements at once?
|
||||
```
|
||||
|
||||
This is a mysql issue.
|
||||
Currently, we don't have a better solution than to completely wipe the database files and start over.
|
||||
To do this, first stop everything by running `sudo docker-compose down`, then remove the `db/` folder in your current folder with `sudo rm -rf db` and start the whole stack again with `sudo docker-compose up -d`.
|
||||
|
||||
## Try it
|
||||
|
||||
Head over to `http://<host-ip or url>/api/v1/info` in a browser.
|
||||
You should see something like this:
|
||||
|
||||
{{< highlight json >}}
|
||||
{
|
||||
"version": "0.13.1+19-e9bc3246ce",
|
||||
"frontend_url": "http://localhost:8080/",
|
||||
"motd": "test",
|
||||
"link_sharing_enabled": true,
|
||||
"max_file_size": "20MB",
|
||||
"registration_enabled": true,
|
||||
"available_migrators": [
|
||||
"wunderlist",
|
||||
"todoist"
|
||||
],
|
||||
"task_attachments_enabled": true
|
||||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
This shows you can reach the api through the api proxy.
|
||||
|
||||
Now head over to `http://<host-ip or url>/` which should show the login mask.
|
||||
|
||||
## Make it persistent
|
||||
|
||||
Currently, Vikunja runs in foreground in your terminal.
|
||||
For a real-world scenario this is not the best way.
|
||||
|
||||
Back in your terminal, stop the stack by pressing `CTRL-C` on your keyboard.
|
||||
Then run `sudo docker-compose up -d` in your again.
|
||||
The `-d` flag at the end of the command will tell docker to run the containers in the background.
|
||||
If you need to check the logs after that, you can run `sudo docker-compose logs`.
|
||||
|
||||
Vikunja does not have any default users, you'll need to register and account.
|
||||
After that, you can use it.
|
||||
|
||||
## Tear it all down
|
||||
|
||||
If you want to completely stop all containers run `sudo docker-compose down` in your terminal.
|
||||
|
||||
## Improve this guide
|
||||
|
||||
We'll happily accept suggestions and improvements for this guide.
|
||||
Please [reach out to us](https://vikunja.io/contact/) if you have any.
|
|
@ -15,11 +15,36 @@ It uses an nginx container or traefik on the host to proxy backend and frontend
|
|||
|
||||
For all available configuration options, see [configuration]({{< ref "config.md">}}).
|
||||
|
||||
## Example with traefik
|
||||
### Redis
|
||||
|
||||
This example assumes [traefik](https://traefik.io) in version 1 installed and configured to [use docker as a configuration provider](https://docs.traefik.io/v1.7/configuration/backends/docker/).
|
||||
To use redis, you'll need to add this to the config examples below:
|
||||
|
||||
### Without redis
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
api:
|
||||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_REDIS_ENABLED: 1
|
||||
VIKUNJA_REDIS_HOST: 'redis:6379'
|
||||
VIKUNJA_CACHE_ENABLED: 1
|
||||
VIKUNJA_CACHE_TYPE: redis
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
redis:
|
||||
image: redis
|
||||
{{< /highlight >}}
|
||||
|
||||
## Example with traefik 2
|
||||
|
||||
This example assumes [traefik](https://traefik.io) version 2 installed and configured to [use docker as a configuration provider](https://docs.traefik.io/providers/docker/).
|
||||
|
||||
We also make a few assumtions here which you'll most likely need to adjust for your traefik setup:
|
||||
|
||||
* Your domain is `vikunja.example.com`
|
||||
* The entrypoint you want to make vikunja available from is called `https`
|
||||
* The tls cert resolver is called `acme`
|
||||
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
@ -42,19 +67,17 @@ services:
|
|||
- db
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.docker.network=web"
|
||||
- "traefik.enable=true"
|
||||
- "traefik.frontend.rule=Host:vikunja.example.com;PathPrefix:/api/v1"
|
||||
- "traefik.port=3456"
|
||||
- "traefik.protocol=http"
|
||||
- "traefik.http.routers.vikunja-api.rule=Host(`vikunja.example.com`) && PathPrefix(`/api/v1`)"
|
||||
- "traefik.http.routers.vikunja-api.entrypoints=https"
|
||||
- "traefik.http.routers.vikunja-api.tls.certResolver=acme"
|
||||
frontend:
|
||||
image: vikunja/frontend
|
||||
labels:
|
||||
- "traefik.docker.network=web"
|
||||
- "traefik.enable=true"
|
||||
- "traefik.frontend.rule=Host:vikunja.example.com;PathPrefix:/"
|
||||
- "traefik.port=80"
|
||||
- "traefik.protocol=http"
|
||||
- "traefik.http.routers.vikunja-frontend.rule=Host(`vikunja.example.com`)"
|
||||
- "traefik.http.routers.vikunja-frontend.entrypoints=https"
|
||||
- "traefik.http.routers.vikunja-frontend.tls.certResolver=acme"
|
||||
networks:
|
||||
- web
|
||||
- default
|
||||
|
@ -76,7 +99,9 @@ networks:
|
|||
external: true
|
||||
{{< /highlight >}}
|
||||
|
||||
### With redis
|
||||
## Example with traefik 1
|
||||
|
||||
This example assumes [traefik](https://traefik.io) in version 1 installed and configured to [use docker as a configuration provider](https://docs.traefik.io/v1.7/configuration/backends/docker/).
|
||||
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
@ -90,10 +115,6 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: vikunja
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_REDIS_ENABLED: 1
|
||||
VIKUNJA_REDIS_HOST: 'redis:6379'
|
||||
VIKUNJA_CACHE_ENABLED: 1
|
||||
VIKUNJA_CACHE_TYPE: redis
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
networks:
|
||||
|
@ -101,7 +122,6 @@ services:
|
|||
- default
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "traefik.docker.network=web"
|
||||
|
@ -132,8 +152,6 @@ services:
|
|||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
command: --max-connections=1000
|
||||
redis:
|
||||
image: redis
|
||||
|
||||
networks:
|
||||
web:
|
||||
|
@ -159,7 +177,7 @@ server {
|
|||
}
|
||||
{{< /highlight >}}
|
||||
|
||||
### Without redis
|
||||
`docker-compose.yml` config:
|
||||
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
@ -172,6 +190,7 @@ services:
|
|||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
restart: unless-stopped
|
||||
api:
|
||||
image: vikunja/api
|
||||
environment:
|
||||
|
@ -184,8 +203,10 @@ services:
|
|||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
frontend:
|
||||
image: vikunja/frontend
|
||||
restart: unless-stopped
|
||||
proxy:
|
||||
image: nginx
|
||||
ports:
|
||||
|
@ -195,49 +216,5 @@ services:
|
|||
depends_on:
|
||||
- api
|
||||
- frontend
|
||||
restart: unless-stopped
|
||||
{{< /highlight >}}
|
||||
|
||||
### With redis
|
||||
|
||||
{{< highlight yaml >}}
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mariadb:10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
redis:
|
||||
image: redis
|
||||
api:
|
||||
image: vikunja/api
|
||||
environment:
|
||||
VIKUNJA_DATABASE_HOST: db
|
||||
VIKUNJA_DATABASE_PASSWORD: supersecret
|
||||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: root
|
||||
VIKUNJA_DATABASE_DATABASE: vikunja
|
||||
VIKUNJA_REDIS_ENABLED: 1
|
||||
VIKUNJA_REDIS_HOST: 'redis:6379'
|
||||
VIKUNJA_CACHE_ENABLED: 1
|
||||
VIKUNJA_CACHE_TYPE: redis
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
frontend:
|
||||
image: vikunja/frontend
|
||||
proxy:
|
||||
image: nginx
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- api
|
||||
- frontend
|
||||
{{< /highlight >}}
|
|
@ -106,7 +106,7 @@ docker run -p 3456:3456 vikunja/api
|
|||
{{< /highlight >}}
|
||||
|
||||
to run with a standard configuration.
|
||||
This will expose
|
||||
This will expose vikunja on port `3456` on the host running the container.
|
||||
|
||||
You can mount a local configuration like so:
|
||||
|
||||
|
@ -117,6 +117,18 @@ docker run -p 3456:3456 -v /path/to/config/on/host.yml:/app/vikunja/config.yml:r
|
|||
Though it is recommended to use eviroment variables or `.env` files to configure Vikunja in docker.
|
||||
See [config]({{< ref "config.md">}}) for a list of available configuration options.
|
||||
|
||||
### Files volume
|
||||
|
||||
By default the container stores all files uploaded and used through vikunja inside of `/app/vikunja/files` which is created as a docker volume.
|
||||
You should mount the volume somewhere to the host to permanently store the files and don't loose them if the container restarts.
|
||||
|
||||
### Setting user and group id of the user running vikunja
|
||||
|
||||
You can set the user and group id of the user running vikunja with the `PUID` and `PGID` evironment variables.
|
||||
This follows the pattern used by [the linuxserver.io](https://docs.linuxserver.io/general/understanding-puid-and-pgid) docker images.
|
||||
|
||||
This is useful to solve general permission problems when host-mounting volumes such as the volume used for task attachments.
|
||||
|
||||
### Docker compose
|
||||
|
||||
To run the backend with a mariadb database you can use this example [docker-compose](https://docs.docker.com/compose/) file:
|
||||
|
@ -132,13 +144,15 @@ services:
|
|||
VIKUNJA_DATABASE_TYPE: mysql
|
||||
VIKUNJA_DATABASE_USER: root
|
||||
VIKUNJA_SERVICE_JWTSECRET: <generated secret>
|
||||
volumes:
|
||||
- ./files:/app/vikunja/files
|
||||
db:
|
||||
image: mariadb:10
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: supersecret
|
||||
MYSQL_DATABASE: vikunja
|
||||
volumes:
|
||||
- ./db:/var/lib/mysql
|
||||
- ./db:/var/lib/mysql
|
||||
{{< /highlight >}}
|
||||
|
||||
See [full docker example]({{< ref "full-docker-example.md">}}) for more varations of this config.
|
||||
|
|
|
@ -17,6 +17,20 @@ Unzip them and store them somewhere your server can access them.
|
|||
|
||||
You also need to configure a rewrite condition to internally redirect all requests to `index.html` which handles all urls.
|
||||
|
||||
## API URL configuration
|
||||
|
||||
By default, the frontend assumes it can reach the api at `/api/v1` relative to the frontend url.
|
||||
This means that if you make the frontend available at, say `https://vikunja.example.com`, it tries to reach the api
|
||||
at `https://vikunja.example.com/api/v1`.
|
||||
In this scenario it is not possible for the frontend and the api to live on seperate servers or even just seperate
|
||||
ports on the same server with [the use of a reverse proxy]({{< ref "reverse-proxies.md">}}).
|
||||
|
||||
To make configurations like this possible, the api url can be set in the `index.html` file of the frontend releases.
|
||||
Just open the file with a text editor - there are comments which will explain how to set the url.
|
||||
|
||||
**Note:** This needs to be done again after every update.
|
||||
(If you have a good idea for a better solution than this, we'd love to [hear it](https://vikunja.io/contact/))
|
||||
|
||||
## Docker
|
||||
|
||||
The docker image is based on nginx and just contains all nessecary files for the frontend.
|
||||
|
@ -31,6 +45,16 @@ which will run the docker image and expose port 80 on the host.
|
|||
|
||||
See [full docker example]({{< ref "full-docker-example.md">}}) for more varations of this config.
|
||||
|
||||
### Setting user and group id of the user running vikunja
|
||||
|
||||
You can set the user and group id of the user running vikunja with the `PUID` and `PGID` evironment variables.
|
||||
This follows the pattern used by [the linuxserver.io](https://docs.linuxserver.io/general/understanding-puid-and-pgid) docker images.
|
||||
|
||||
### API URL configuration in docker
|
||||
|
||||
When running the frontend with docker, it is possible to set the environment variable `$VIKUNJA_API_URL` to the api url.
|
||||
It is therefore not needed to change the url manually inside the docker container.
|
||||
|
||||
## NGINX
|
||||
|
||||
Below are two example configurations which you can put in your `nginx.conf`:
|
||||
|
|
|
@ -77,6 +77,7 @@ This document describes the different errors Vikunja can return.
|
|||
| 4015 | 404 | The task comment does not exist. |
|
||||
| 4016 | 403 | Invalid task field. |
|
||||
| 4017 | 403 | Invalid task filter comparator. |
|
||||
| 4018 | 403 | Invalid task filter concatinator. |
|
||||
|
||||
### Namespace
|
||||
|
||||
|
@ -128,3 +129,4 @@ This document describes the different errors Vikunja can return.
|
|||
|-----------|------------------|-------------|
|
||||
| 10001 | 404 | The bucket does not exist. |
|
||||
| 10002 | 400 | The bucket does not belong to that list. |
|
||||
| 10003 | 412 | You cannot remove the last bucket on a list. |
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit cbc6a98e902516fcb0c8644283e17ebf5e74a2f3
|
||||
Subproject commit 88977f461bc7cb0671cdbfd64c91cf0f13419865
|
23
go.mod
23
go.mod
|
@ -33,10 +33,9 @@ require (
|
|||
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
|
||||
github.com/go-openapi/jsonreference v0.19.3 // indirect
|
||||
github.com/go-openapi/spec v0.19.4 // indirect
|
||||
github.com/go-redis/redis/v7 v7.2.0
|
||||
github.com/go-redis/redis/v7 v7.3.0
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.1.1
|
||||
github.com/golang/protobuf v1.3.5 // indirect
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.2.0
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
|
||||
github.com/imdario/mergo v0.3.9
|
||||
|
@ -45,10 +44,9 @@ require (
|
|||
github.com/labstack/echo/v4 v4.1.16
|
||||
github.com/labstack/gommon v0.3.0
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
|
||||
github.com/lib/pq v1.4.0
|
||||
github.com/lib/pq v1.6.0
|
||||
github.com/mailru/easyjson v0.7.0 // indirect
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.4
|
||||
github.com/onsi/ginkgo v1.12.0 // indirect
|
||||
|
@ -56,29 +54,28 @@ require (
|
|||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/pquerna/otp v1.2.0
|
||||
github.com/prometheus/client_golang v1.5.1
|
||||
github.com/prometheus/client_golang v1.6.0
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
|
||||
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd
|
||||
github.com/spf13/afero v1.2.2
|
||||
github.com/spf13/cobra v0.0.7
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.6.3
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.6.0
|
||||
github.com/swaggo/swag v1.6.3
|
||||
github.com/ulule/limiter/v3 v3.5.0
|
||||
github.com/urfave/cli v1.22.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200422194213-44a606286825
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect
|
||||
golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa // indirect
|
||||
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef // indirect
|
||||
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
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a
|
||||
src.techknowlogick.com/xgo v0.0.0-20200408234745-bb0faa361273
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
src.techknowlogick.com/xgo v0.0.0-20200514233805-209a5cf70012
|
||||
src.techknowlogick.com/xormigrate v1.2.0
|
||||
xorm.io/builder v0.3.7
|
||||
xorm.io/core v0.7.3
|
||||
|
|
216
go.sum
216
go.sum
|
@ -9,8 +9,19 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg=
|
|||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
|
||||
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
code.vikunja.io/web v0.0.0-20200208214421-c90649369427 h1:6ps5r0OxZNRdmCavh1k/xMwftN27hHauo+EtdTGxLug=
|
||||
code.vikunja.io/web v0.0.0-20200208214421-c90649369427/go.mod h1:cuP1/ieGWAZzgQGw+QPt6Y5F0fVb/8Ol5NV4QSezGdo=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitea.com/xorm/tests v0.7.0 h1:pFcaxTGGAWw3rDuVfhBdyr+mX1uzdTtncyAKxkCQ/IE=
|
||||
|
@ -19,6 +30,7 @@ gitea.com/xorm/xorm-redis-cache v0.2.0 h1:qglRHt6/7vJmDeld6j+n10M9PmruAh+Le2lgNr
|
|||
gitea.com/xorm/xorm-redis-cache v0.2.0/go.mod h1:juYdjkmIKvLbPkdfBVKGVJ2daFQIJAgKsn4mL4ZK8Zk=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
|
@ -35,8 +47,13 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
|
|||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
|
@ -47,6 +64,8 @@ github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
|
|||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee h1:BnPxIde0gjtTnc9Er7cxvBk8DHLWhEux0SxayC8dP6I=
|
||||
|
@ -62,6 +81,7 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe
|
|||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
|
@ -86,6 +106,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
|
|||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
|
@ -103,6 +124,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
|||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
|
@ -133,13 +155,15 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
|||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs=
|
||||
github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/go-redis/redis/v7 v7.3.0 h1:3oHqd0W7f/VLKBxeYTEpqdMUsmMectngjM9OtoRoIgg=
|
||||
github.com/go-redis/redis/v7 v7.3.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
|
||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.1.1 h1:SBIfzULODQQ7JV6AD931MvAz8pnkk6QCfMIcoOBDaXQ=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.1.1/go.mod h1:RZctY24ixituGC73XlAV1gkCwYMVwiSwPm26MNlQIhE=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.2.0 h1:FGAW3z5UzmrZGjR/dZp1u3Tbld0SDmirLO4RrR5++7Q=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.2.0/go.mod h1:RZctY24ixituGC73XlAV1gkCwYMVwiSwPm26MNlQIhE=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
|
@ -151,14 +175,19 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
|||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
|
@ -167,26 +196,53 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
|||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf h1:vc7Dmrk4JwS0ZPS6WZvWlwDflgDTA26jItmbSj83nug=
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
|
||||
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
|
@ -196,6 +252,18 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
|
|||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
|
||||
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
|
||||
github.com/jackc/pgx v3.6.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.2.0 h1:lzPl/30ZLkTveYsYZPKMcgXc8MbnE6RsTd4F9KgiLtk=
|
||||
github.com/jcmturner/gokrb5/v8 v8.2.0/go.mod h1:T1hnNppQsBtxW0tCHMHTkAt8n/sABdzZgZdoFrZaZNM=
|
||||
github.com/jcmturner/rpc/v2 v2.0.2 h1:gMB4IwRXYsWw4Bc6o/az2HJgFUA1ffSh90i26ZJ6Xl0=
|
||||
github.com/jcmturner/rpc/v2 v2.0.2/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jeffbean/tail v1.0.1 h1:mRuCwa9iq5kH1SBFAIWWFsg+NEi5+VVEf2E2ORVkKp8=
|
||||
github.com/jeffbean/tail v1.0.1/go.mod h1:+MhJ+VPZMpv8Ui6WRzpJFuWFKxBCZgVOo5HAmlw1sFc=
|
||||
github.com/jgautheron/goconst v0.0.0-20200227150835-cda7ea3bf591 h1:x/BpEhm6aL26o4TLtcU0loJ7B3+69jielrGc70V7Yb4=
|
||||
|
@ -244,8 +312,10 @@ github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
|||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc=
|
||||
github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw=
|
||||
github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.6.0 h1:I5DPxhYJChW9KYc66se+oKFFQX6VuQrKiprsX6ivRZc=
|
||||
github.com/lib/pq v1.6.0/go.mod h1:4vXEAYvW1fRQ2/FhZ78H73A60MHw1geSm145z2mdY1g=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
|
@ -256,10 +326,12 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
|
|||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
|
||||
|
@ -279,15 +351,20 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJK
|
|||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
|
@ -310,6 +387,7 @@ github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT
|
|||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
|
||||
|
@ -323,6 +401,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
|
||||
github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
|
@ -330,8 +409,8 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+Ci
|
|||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
|
||||
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A=
|
||||
github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU=
|
||||
|
@ -354,13 +433,16 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
|
|||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
|
||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shopspring/decimal v0.0.0-20191009025716-f1972eb1d1f5/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk=
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
|
@ -382,8 +464,8 @@ github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
|||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
|
||||
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
|
@ -393,8 +475,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
|||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
|
||||
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
|
||||
github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
|
@ -406,6 +488,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
|
|||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
|
||||
|
@ -444,35 +528,46 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK
|
|||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 h1:DOmugCavvUtnUD114C1Wh+UgTgQZ4pMLzXxi1pSt+/Y=
|
||||
golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904 h1:bXoxMPcSLOq08zI3/c5dEBT6lE4eh+jOh886GHrn6V8=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200420192832-a76a400e3025 h1:U821GPBRQKVHpr6Os5UDBgF2FveIjlZYbz+e2n5wmmQ=
|
||||
golang.org/x/crypto v0.0.0-20200420192832-a76a400e3025/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a h1:y6sBfNd1b9Wy08a6K1Z1DZc4aXABUN5TKjkYhz7UKmo=
|
||||
golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200422194213-44a606286825 h1:dSChiwOTvzwbHFTMq2l6uRardHH7/E6SqEkqccinS/o=
|
||||
golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
|
@ -481,13 +576,16 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r
|
|||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
|
@ -499,12 +597,15 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwL
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -512,22 +613,28 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190609082536-301114b31cce/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -535,14 +642,14 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa h1:mQTN3ECqfsViCNBgq+A40vdwhkGykrrQlYe3mPj6BoU=
|
||||
golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
|
||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -556,15 +663,26 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
|||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190608022120-eacb66d2a7c3/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628034336-212fb13d595e h1:ZlQjfVdpDxeqxRfmO30CdqWWzTvgRCj0MxaUVfxEG1k=
|
||||
golang.org/x/tools v0.0.0-20190628034336-212fb13d595e/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef h1:RHORRhs540cYZYrzgU2CPUyykkwZM78hGdzocOo9P8A=
|
||||
|
@ -574,18 +692,41 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
|
||||
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
|
@ -598,12 +739,18 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
|
|||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1 h1:70AthpjunwzUiarMHyED52mj9UwtAnE89l1Gmrt3EU0=
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1/go.mod h1:EUzikiKadqXWcD1AzJLagx0j/BeeWGtn++04Xniyg44=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo=
|
||||
gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q=
|
||||
gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4=
|
||||
gopkg.in/jcmturner/gokrb5.v7 v7.5.0/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM=
|
||||
gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
|
@ -618,13 +765,20 @@ gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
|||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a h1:LJwr7TCTghdatWv40WobzlKXc9c4s8oGa7QKJUtHhWA=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
src.techknowlogick.com/xgo v0.0.0-20200408234745-bb0faa361273 h1:dE6ry9rVwDn3soD4wPCXqEG60AZTuhniZzHdnj3c+74=
|
||||
src.techknowlogick.com/xgo v0.0.0-20200408234745-bb0faa361273/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
src.techknowlogick.com/xgo v0.0.0-20200514233805-209a5cf70012 h1:k1/qGRpsaGka4IHT2UIUdPqM6+oo3O7+8S3ACN2kJZQ=
|
||||
src.techknowlogick.com/xgo v0.0.0-20200514233805-209a5cf70012/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
|
||||
src.techknowlogick.com/xormigrate v1.2.0 h1:bq9JaI48bxB+OddMghicjmV7sGmBUogJq4HmTN0DOcw=
|
||||
src.techknowlogick.com/xormigrate v1.2.0/go.mod h1:7so27LAfBRqAxbma5jKYeL4ykVG1Jhsv9ncSq1KBCs4=
|
||||
xorm.io/builder v0.3.7 h1:2pETdKRK+2QG4mLX4oODHEhn5Z8j1m8sXa7jfu+/SZI=
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(testmailCmd)
|
||||
}
|
||||
|
||||
var testmailCmd = &cobra.Command{
|
||||
Use: "testmail [email]",
|
||||
Short: "Send a test mail using the configured smtp connection",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
log.Info("Sending testmail...")
|
||||
email := args[0]
|
||||
if err := mail.SendTestMail(email); err != nil {
|
||||
log.Errorf("Error sending test mail: %s", err.Error())
|
||||
return
|
||||
}
|
||||
log.Info("Testmail successfully sent.")
|
||||
},
|
||||
}
|
|
@ -48,6 +48,7 @@ const (
|
|||
ServiceEnableTaskAttachments Key = `service.enabletaskattachments`
|
||||
ServiceTimeZone Key = `service.timezone`
|
||||
ServiceEnableTaskComments Key = `service.enabletaskcomments`
|
||||
ServiceEnableTotp Key = `service.enabletotp`
|
||||
|
||||
DatabaseType Key = `database.type`
|
||||
DatabaseHost Key = `database.host`
|
||||
|
@ -101,6 +102,10 @@ const (
|
|||
MigrationWunderlistClientID Key = `migration.wunderlist.clientid`
|
||||
MigrationWunderlistClientSecret Key = `migration.wunderlist.clientsecret`
|
||||
MigrationWunderlistRedirectURL Key = `migration.wunderlist.redirecturl`
|
||||
MigrationTodoistEnable Key = `migration.todoist.enable`
|
||||
MigrationTodoistClientID Key = `migration.todoist.clientid`
|
||||
MigrationTodoistClientSecret Key = `migration.todoist.clientsecret`
|
||||
MigrationTodoistRedirectURL Key = `migration.todoist.redirecturl`
|
||||
|
||||
CorsEnable Key = `cors.enable`
|
||||
CorsOrigins Key = `cors.origins`
|
||||
|
@ -108,6 +113,11 @@ const (
|
|||
|
||||
AvatarProvider Key = `avatar.provider`
|
||||
AvatarGravaterExpiration Key = `avatar.gravatarexpiration`
|
||||
|
||||
BackgroundsEnabled Key = `backgrounds.enabled`
|
||||
BackgroundsUnsplashEnabled Key = `backgrounds.providers.unsplash.enabled`
|
||||
BackgroundsUnsplashAccessToken Key = `backgrounds.providers.unsplash.accesstoken`
|
||||
BackgroundsUnsplashApplicationID Key = `backgrounds.providers.unsplash.applicationid`
|
||||
)
|
||||
|
||||
// GetString returns a string config value
|
||||
|
@ -179,6 +189,7 @@ func InitDefaultConfig() {
|
|||
ServiceEnableTaskAttachments.setDefault(true)
|
||||
ServiceTimeZone.setDefault("GMT")
|
||||
ServiceEnableTaskComments.setDefault(true)
|
||||
ServiceEnableTotp.setDefault(true)
|
||||
|
||||
// Database
|
||||
DatabaseType.setDefault("sqlite")
|
||||
|
@ -235,9 +246,13 @@ func InitDefaultConfig() {
|
|||
CorsMaxAge.setDefault(0)
|
||||
// Migration
|
||||
MigrationWunderlistEnable.setDefault(false)
|
||||
MigrationTodoistEnable.setDefault(false)
|
||||
// Avatar
|
||||
AvatarProvider.setDefault("gravatar")
|
||||
AvatarGravaterExpiration.setDefault(3600)
|
||||
// List Backgrounds
|
||||
BackgroundsEnabled.setDefault(false)
|
||||
BackgroundsUnsplashEnabled.setDefault(false)
|
||||
}
|
||||
|
||||
// InitConfig initializes the config, sets defaults etc.
|
||||
|
|
|
@ -101,3 +101,107 @@
|
|||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 18
|
||||
title: testbucket18
|
||||
list_id: 5
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 19
|
||||
title: testbucket19
|
||||
list_id: 21
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 20
|
||||
title: testbucket20
|
||||
list_id: 22
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 21
|
||||
title: testbucket21
|
||||
list_id: 3
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
# Duplicate buckets to make deletion of one of them possible
|
||||
- id: 22
|
||||
title: testbucket22
|
||||
list_id: 6
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 23
|
||||
title: testbucket23
|
||||
list_id: 7
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 24
|
||||
title: testbucket24
|
||||
list_id: 8
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 25
|
||||
title: testbucket25
|
||||
list_id: 9
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 26
|
||||
title: testbucket26
|
||||
list_id: 10
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 27
|
||||
title: testbucket27
|
||||
list_id: 11
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 28
|
||||
title: testbucket28
|
||||
list_id: 12
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 29
|
||||
title: testbucket29
|
||||
list_id: 13
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 30
|
||||
title: testbucket30
|
||||
list_id: 14
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 31
|
||||
title: testbucket31
|
||||
list_id: 15
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 32
|
||||
title: testbucket32
|
||||
list_id: 16
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
- id: 33
|
||||
title: testbucket33
|
||||
list_id: 17
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
# This bucket is the last one in its list
|
||||
- id: 34
|
||||
title: testbucket34
|
||||
list_id: 18
|
||||
created_by_id: 1
|
||||
created: 1587244432
|
||||
updated: 1587244432
|
||||
|
|
|
@ -1,83 +1,83 @@
|
|||
- id: 1
|
||||
name: testnamespace
|
||||
title: testnamespace
|
||||
description: Lorem Ipsum
|
||||
owner_id: 1
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 2
|
||||
name: testnamespace2
|
||||
title: testnamespace2
|
||||
description: Lorem Ipsum
|
||||
owner_id: 2
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 3
|
||||
name: testnamespace3
|
||||
title: testnamespace3
|
||||
description: Lorem Ipsum
|
||||
owner_id: 3
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 6
|
||||
name: testnamespace6
|
||||
title: testnamespace6
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 7
|
||||
name: testnamespace7
|
||||
title: testnamespace7
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 8
|
||||
name: testnamespace8
|
||||
title: testnamespace8
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 9
|
||||
name: testnamespace9
|
||||
title: testnamespace9
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 10
|
||||
name: testnamespace10
|
||||
title: testnamespace10
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 11
|
||||
name: testnamespace11
|
||||
title: testnamespace11
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 12
|
||||
name: testnamespace12
|
||||
title: testnamespace12
|
||||
description: Lorem Ipsum
|
||||
owner_id: 6
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 13
|
||||
name: testnamespace13
|
||||
title: testnamespace13
|
||||
description: Lorem Ipsum
|
||||
owner_id: 7
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 14
|
||||
name: testnamespace14
|
||||
title: testnamespace14
|
||||
description: Lorem Ipsum
|
||||
owner_id: 7
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 15
|
||||
name: testnamespace15
|
||||
title: testnamespace15
|
||||
description: Lorem Ipsum
|
||||
owner_id: 13
|
||||
updated: 0
|
||||
created: 0
|
||||
- id: 16
|
||||
name: Archived testnamespace16
|
||||
title: Archived testnamespace16
|
||||
owner_id: 1
|
||||
is_archived: 1
|
||||
updated: 0
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- id: 1
|
||||
text: 'task #1'
|
||||
title: 'task #1'
|
||||
description: 'Lorem Ipsum'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
|
@ -9,7 +9,7 @@
|
|||
updated: 1543626724
|
||||
bucket_id: 1
|
||||
- id: 2
|
||||
text: 'task #2 done'
|
||||
title: 'task #2 done'
|
||||
done: true
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -18,7 +18,7 @@
|
|||
updated: 1543626724
|
||||
bucket_id: 1
|
||||
- id: 3
|
||||
text: 'task #3 high prio'
|
||||
title: 'task #3 high prio'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -28,7 +28,7 @@
|
|||
priority: 100
|
||||
bucket_id: 2
|
||||
- id: 4
|
||||
text: 'task #4 low prio'
|
||||
title: 'task #4 low prio'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -38,7 +38,7 @@
|
|||
priority: 1
|
||||
bucket_id: 2
|
||||
- id: 5
|
||||
text: 'task #5 higher due date'
|
||||
title: 'task #5 higher due date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -48,7 +48,7 @@
|
|||
due_date_unix: 1543636724
|
||||
bucket_id: 2
|
||||
- id: 6
|
||||
text: 'task #6 lower due date'
|
||||
title: 'task #6 lower due date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -58,7 +58,7 @@
|
|||
due_date_unix: 1543616724
|
||||
bucket_id: 3
|
||||
- id: 7
|
||||
text: 'task #7 with start date'
|
||||
title: 'task #7 with start date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -68,7 +68,7 @@
|
|||
start_date_unix: 1544600000
|
||||
bucket_id: 3
|
||||
- id: 8
|
||||
text: 'task #8 with end date'
|
||||
title: 'task #8 with end date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -78,7 +78,7 @@
|
|||
end_date_unix: 1544700000
|
||||
bucket_id: 3
|
||||
- id: 9
|
||||
text: 'task #9 with start and end date'
|
||||
title: 'task #9 with start and end date'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
|
@ -87,224 +87,252 @@
|
|||
updated: 1543626724
|
||||
start_date_unix: 1544600000
|
||||
end_date_unix: 1544700000
|
||||
bucket_id: 1
|
||||
- id: 10
|
||||
text: 'task #10 basic'
|
||||
title: 'task #10 basic'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 10
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 11
|
||||
text: 'task #11 basic'
|
||||
title: 'task #11 basic'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 11
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 12
|
||||
text: 'task #12 basic'
|
||||
title: 'task #12 basic'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 12
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 13
|
||||
text: 'task #13 basic other list'
|
||||
title: 'task #13 basic other list'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 2
|
||||
index: 1
|
||||
bucket_id: 4
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 14
|
||||
text: 'task #14 basic other list'
|
||||
title: 'task #14 basic other list'
|
||||
done: false
|
||||
created_by_id: 5
|
||||
list_id: 5
|
||||
index: 1
|
||||
bucket_id: 18
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 15
|
||||
text: 'task #15'
|
||||
title: 'task #15'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 6
|
||||
index: 1
|
||||
bucket_id: 6
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 16
|
||||
text: 'task #16'
|
||||
title: 'task #16'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 7
|
||||
index: 1
|
||||
bucket_id: 7
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 17
|
||||
text: 'task #17'
|
||||
title: 'task #17'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 8
|
||||
index: 1
|
||||
bucket_id: 8
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 18
|
||||
text: 'task #18'
|
||||
title: 'task #18'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 9
|
||||
index: 1
|
||||
bucket_id: 9
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 19
|
||||
text: 'task #19'
|
||||
title: 'task #19'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 10
|
||||
index: 1
|
||||
bucket_id: 10
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 20
|
||||
text: 'task #20'
|
||||
title: 'task #20'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 11
|
||||
index: 1
|
||||
bucket_id: 11
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 21
|
||||
text: 'task #21'
|
||||
title: 'task #21'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 12
|
||||
index: 1
|
||||
bucket_id: 12
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 22
|
||||
text: 'task #22'
|
||||
title: 'task #22'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 13
|
||||
index: 1
|
||||
bucket_id: 13
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 23
|
||||
text: 'task #23'
|
||||
title: 'task #23'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 14
|
||||
index: 1
|
||||
bucket_id: 14
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 24
|
||||
text: 'task #24'
|
||||
title: 'task #24'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 15
|
||||
index: 1
|
||||
bucket_id: 15
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 25
|
||||
text: 'task #25'
|
||||
title: 'task #25'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 16
|
||||
index: 1
|
||||
bucket_id: 16
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 26
|
||||
text: 'task #26'
|
||||
title: 'task #26'
|
||||
done: false
|
||||
created_by_id: 6
|
||||
list_id: 17
|
||||
index: 1
|
||||
bucket_id: 17
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 27
|
||||
text: 'task #27 with reminders'
|
||||
title: 'task #27 with reminders'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 12
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 28
|
||||
text: 'task #28 with repeat after'
|
||||
title: 'task #28 with repeat after'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
repeat_after: 3600
|
||||
list_id: 1
|
||||
index: 13
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 29
|
||||
text: 'task #29 with parent task (1)'
|
||||
title: 'task #29 with parent task (1)'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 14
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 30
|
||||
text: 'task #30 with assignees'
|
||||
title: 'task #30 with assignees'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 15
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 31
|
||||
text: 'task #31 with color'
|
||||
title: 'task #31 with color'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 16
|
||||
hex_color: f0f0f0
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 32
|
||||
text: 'task #32'
|
||||
title: 'task #32'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 3
|
||||
index: 1
|
||||
bucket_id: 21
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 33
|
||||
text: 'task #33 with percent done'
|
||||
title: 'task #33 with percent done'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
index: 17
|
||||
percent_done: 0.5
|
||||
bucket_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
# This task is forbidden for user1
|
||||
- id: 34
|
||||
text: 'task #34'
|
||||
title: 'task #34'
|
||||
done: false
|
||||
created_by_id: 13
|
||||
list_id: 20
|
||||
index: 20
|
||||
bucket_id: 5
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 35
|
||||
text: 'task #35'
|
||||
title: 'task #35'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 21
|
||||
index: 1
|
||||
bucket_id: 19
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 36
|
||||
text: 'task #36'
|
||||
title: 'task #36'
|
||||
done: false
|
||||
created_by_id: 1
|
||||
list_id: 22
|
||||
index: 1
|
||||
bucket_id: 20
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
|
||||
|
|
|
@ -50,3 +50,20 @@ func IsErrFileIsTooLarge(err error) bool {
|
|||
_, ok := err.(ErrFileIsTooLarge)
|
||||
return ok
|
||||
}
|
||||
|
||||
// ErrFileIsNotUnsplashFile defines an error where a file is not downloaded from unsplash.
|
||||
// Used in cases whenever unsplash information about a file is requested, but the file was not downloaded from unsplash.
|
||||
type ErrFileIsNotUnsplashFile struct {
|
||||
FileID int64
|
||||
}
|
||||
|
||||
// Error is the error implementation of ErrFileIsNotUnsplashFile
|
||||
func (err ErrFileIsNotUnsplashFile) Error() string {
|
||||
return fmt.Sprintf("file was not downloaded from unsplash [FileID: %d]", err.FileID)
|
||||
}
|
||||
|
||||
//IsErrFileIsNotUnsplashFile checks if an error is ErrFileIsNotUnsplashFile
|
||||
func IsErrFileIsNotUnsplashFile(err error) bool {
|
||||
_, ok := err.(ErrFileIsNotUnsplashFile)
|
||||
return ok
|
||||
}
|
||||
|
|
|
@ -107,12 +107,12 @@ func TestArchived(t *testing.T) {
|
|||
|
||||
t.Run("namespace", func(t *testing.T) {
|
||||
t.Run("not editable", func(t *testing.T) {
|
||||
_, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"name":"TestIpsum","is_archived":true}`)
|
||||
_, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":true}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||
})
|
||||
t.Run("unarchivable", func(t *testing.T) {
|
||||
rec, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"name":"TestIpsum","is_archived":false}`)
|
||||
rec, err := testNamespaceHandler.testUpdateWithUser(nil, map[string]string{"namespace": "16"}, `{"title":"TestIpsum","is_archived":false}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"is_archived":false`)
|
||||
})
|
||||
|
@ -124,12 +124,12 @@ func TestArchived(t *testing.T) {
|
|||
t.Run("should not appear in the list", func(t *testing.T) {
|
||||
rec, err := testNamespaceHandler.testReadAllWithUser(nil, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `"name":"Archived testnamespace16"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"title":"Archived testnamespace16"`)
|
||||
})
|
||||
t.Run("should appear in the list if explicitly requested", func(t *testing.T) {
|
||||
rec, err := testNamespaceHandler.testReadAllWithUser(url.Values{"is_archived": []string{"true"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"name":"Archived testnamespace16"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Archived testnamespace16"`)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -138,7 +138,7 @@ func TestArchived(t *testing.T) {
|
|||
taskTests := func(taskID string, errCode int, t *testing.T) {
|
||||
t.Run("task", func(t *testing.T) {
|
||||
t.Run("edit task", func(t *testing.T) {
|
||||
_, err := testTaskHandler.testUpdateWithUser(nil, map[string]string{"listtask": taskID}, `{"text":"TestIpsum"}`)
|
||||
_, err := testTaskHandler.testUpdateWithUser(nil, map[string]string{"listtask": taskID}, `{"title":"TestIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, errCode)
|
||||
})
|
||||
|
@ -202,7 +202,7 @@ func TestArchived(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||
})
|
||||
t.Run("no new tasks", func(t *testing.T) {
|
||||
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"list": "21"}, `{"text":"Lorem"}`)
|
||||
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"list": "21"}, `{"title":"Lorem"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceIsArchived)
|
||||
})
|
||||
|
@ -222,7 +222,7 @@ func TestArchived(t *testing.T) {
|
|||
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
|
||||
})
|
||||
t.Run("no new tasks", func(t *testing.T) {
|
||||
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"list": "22"}, `{"text":"Lorem"}`)
|
||||
_, err := testTaskHandler.testCreateWithUser(nil, map[string]string{"list": "22"}, `{"title":"Lorem"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListIsArchived)
|
||||
})
|
||||
|
|
|
@ -133,7 +133,7 @@ func TestBucket(t *testing.T) {
|
|||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "1"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "1", "bucket": "1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
@ -145,70 +145,70 @@ func TestBucket(t *testing.T) {
|
|||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
// Owned by user13
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "5"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "20", "bucket": "5"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "6"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "6", "bucket": "6"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "7"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "7", "bucket": "7"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "8"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "8", "bucket": "8"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "9"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "9", "bucket": "9"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "10"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "10", "bucket": "10"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "11"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "11", "bucket": "11"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "12"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "12", "bucket": "12"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "13"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "13", "bucket": "13"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "14"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "14", "bucket": "14"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "15"})
|
||||
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "15", "bucket": "15"})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "16"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "16", "bucket": "16"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"bucket": "17"})
|
||||
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "17", "bucket": "17"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
|
||||
})
|
||||
|
|
|
@ -54,6 +54,84 @@ func TestLinkSharing(t *testing.T) {
|
|||
SharedByID: 1,
|
||||
}
|
||||
|
||||
t.Run("New Link Share", func(t *testing.T) {
|
||||
testHandler := webHandlerTest{
|
||||
user: &testuser1,
|
||||
strFunc: func() handler.CObject {
|
||||
return &models.LinkSharing{}
|
||||
},
|
||||
t: t,
|
||||
}
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":0}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"right":2}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Read only access", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":0}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":1}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"right":2}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Write access", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":0}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"right":2}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Admin access", func(t *testing.T) {
|
||||
t.Run("read only", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":0}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("write", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":1}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
t.Run("admin", func(t *testing.T) {
|
||||
req, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"right":2}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Body.String(), `"hash":`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Lists", func(t *testing.T) {
|
||||
testHandlerListReadOnly := webHandlerTest{
|
||||
linkShare: linkshareRead,
|
||||
|
@ -507,36 +585,36 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"listtask": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"listtask": "13"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"listtask": "13"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"listtask": "32"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"listtask": "32"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
})
|
||||
|
@ -769,34 +847,34 @@ func TestLinkSharing(t *testing.T) {
|
|||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceReadOnly.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`)
|
||||
_, err := testHandlerNamespaceReadOnly.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceWrite.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`)
|
||||
_, err := testHandlerNamespaceWrite.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceAdmin.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`)
|
||||
_, err := testHandlerNamespaceAdmin.testCreateWithLinkShare(nil, nil, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
})
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Shared readonly", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"name":"LoremIpsum"}`)
|
||||
_, err := testHandlerNamespaceReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared write", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"name":"LoremIpsum"}`)
|
||||
_, err := testHandlerNamespaceWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared admin", func(t *testing.T) {
|
||||
_, err := testHandlerNamespaceAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"name":"LoremIpsum"}`)
|
||||
_, err := testHandlerNamespaceAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"LoremIpsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
|
|
|
@ -89,39 +89,73 @@ func TestTaskCollection(t *testing.T) {
|
|||
assert.NotContains(t, rec.Body.String(), `task #13`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
t.Run("Search case insensitive", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"tASk #6"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `task #1`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #2`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #3`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #4`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #5`)
|
||||
assert.Contains(t, rec.Body.String(), `task #6`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #7`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #8`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #9`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #10`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #11`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #12`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #13`)
|
||||
assert.NotContains(t, rec.Body.String(), `task #14`)
|
||||
})
|
||||
t.Run("Sort Order", func(t *testing.T) {
|
||||
// TODO: Add more cases
|
||||
// should equal priority asc
|
||||
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,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
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,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":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,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"title":"task #6 lower due date`)
|
||||
})
|
||||
// Due date without unix suffix
|
||||
t.Run("by duedate asc without _unix 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":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
t.Run("by due_date without _unix 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":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
t.Run("by duedate desc without _unix 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":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"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_unix"}, "order_by": []string{"asc"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
t.Run("invalid sort parameter", func(t *testing.T) {
|
||||
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
|
||||
|
@ -137,10 +171,10 @@ func TestTaskCollection(t *testing.T) {
|
|||
// Invalid parameter should not sort at all
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, urlParams)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
})
|
||||
t.Run("Date range", func(t *testing.T) {
|
||||
|
@ -207,7 +241,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
// If no start date but an end date is specified, this should be null
|
||||
// since we don't have any tasks in the fixtures with an end date >
|
||||
// the current date.
|
||||
assert.Equal(t, "null\n", rec.Body.String())
|
||||
assert.Equal(t, "[]\n", rec.Body.String())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -270,42 +304,42 @@ 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,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
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,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":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,"text":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":1,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":null,"due_date":null,"reminder_dates":null,"list_id":1,"repeat_after":0,"priority":100,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
// should equal duedate asc
|
||||
t.Run("by due_date", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"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_unix"}, "order_by": []string{"asc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":null,"due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":null,"due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"priority":0,"start_date":null,"end_date":null,"assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"created_by":{"id":1,"username":"user1","created":null,"updated":null}}]`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`)
|
||||
assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
})
|
||||
t.Run("Date range", func(t *testing.T) {
|
||||
|
@ -372,7 +406,7 @@ func TestTaskCollection(t *testing.T) {
|
|||
// If no start date but an end date is specified, this should be null
|
||||
// since we don't have any tasks in the fixtures with an end date >
|
||||
// the current date.
|
||||
assert.Equal(t, "null\n", rec.Body.String())
|
||||
assert.Equal(t, "[]\n", rec.Body.String())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -36,11 +36,11 @@ func TestTask(t *testing.T) {
|
|||
// ^TestTask$/^Update$/^Update_task_items$/^Removing_Assignees_null$
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
t.Run("Update task items", func(t *testing.T) {
|
||||
t.Run("Text", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`)
|
||||
t.Run("Title", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"text":"task #1"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
assert.NotContains(t, rec.Body.String(), `"title":"task #1"`)
|
||||
})
|
||||
t.Run("Description", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`)
|
||||
|
@ -195,78 +195,78 @@ func TestTask(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "99999"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "99999"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "14"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "14"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "15"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "15"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "16"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "16"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "17"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "17"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "18"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "18"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "19"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "19"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "20"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "20"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "21"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "21"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "22"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "22"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "23"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "23"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "24"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "24"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "25"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "25"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "26"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "26"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Move to other list", func(t *testing.T) {
|
||||
|
@ -390,100 +390,100 @@ func TestTask(t *testing.T) {
|
|||
})
|
||||
t.Run("Create", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Nonexisting", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
|
||||
})
|
||||
t.Run("Rights check", func(t *testing.T) {
|
||||
t.Run("Forbidden", func(t *testing.T) {
|
||||
// Owned by user13
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via Team write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via Team admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via User readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via User write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via User admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
|
||||
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"text":"Lorem Ipsum"}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"text":"Lorem Ipsum"}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"title":"Lorem Ipsum"}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
|
||||
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
|
||||
})
|
||||
})
|
||||
t.Run("Bucket", func(t *testing.T) {
|
||||
t.Run("Normal", func(t *testing.T) {
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":2}`)
|
||||
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum","bucket_id":2}`)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `"bucket_id":2`)
|
||||
assert.NotContains(t, rec.Body.String(), `"bucket_id":1`)
|
||||
})
|
||||
t.Run("Different List", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":4}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum","bucket_id":4}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotBelongToList)
|
||||
})
|
||||
t.Run("Nonexisting Bucket", func(t *testing.T) {
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum","bucket_id":9999}`)
|
||||
_, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem Ipsum","bucket_id":9999}`)
|
||||
assert.Error(t, err)
|
||||
assertHandlerErrorCode(t, err, models.ErrCodeBucketDoesNotExist)
|
||||
})
|
||||
|
|
|
@ -27,6 +27,16 @@ import (
|
|||
// Queue is the mail queue
|
||||
var Queue chan *gomail.Message
|
||||
|
||||
func getDialer() *gomail.Dialer {
|
||||
d := gomail.NewDialer(config.MailerHost.GetString(), config.MailerPort.GetInt(), config.MailerUsername.GetString(), config.MailerPassword.GetString())
|
||||
// #nosec
|
||||
d.TLSConfig = &tls.Config{
|
||||
InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool(),
|
||||
ServerName: config.MailerHost.GetString(),
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// StartMailDaemon starts the mail daemon
|
||||
func StartMailDaemon() {
|
||||
Queue = make(chan *gomail.Message, config.MailerQueuelength.GetInt())
|
||||
|
@ -41,9 +51,7 @@ func StartMailDaemon() {
|
|||
}
|
||||
|
||||
go func() {
|
||||
d := gomail.NewDialer(config.MailerHost.GetString(), config.MailerPort.GetInt(), config.MailerUsername.GetString(), config.MailerPassword.GetString())
|
||||
// #nosec
|
||||
d.TLSConfig = &tls.Config{InsecureSkipVerify: config.MailerSkipTLSVerify.GetBool()}
|
||||
d := getDialer()
|
||||
|
||||
var s gomail.SendCloser
|
||||
var err error
|
||||
|
|
|
@ -53,6 +53,30 @@ type header struct {
|
|||
Content string
|
||||
}
|
||||
|
||||
// SendTestMail sends a test mail to a receipient.
|
||||
// It works without a queue.
|
||||
func SendTestMail(to string) error {
|
||||
if config.MailerHost.GetString() == "" {
|
||||
log.Warning("Mailer seems to be not configured! Please see the config docs for more details.")
|
||||
return nil
|
||||
}
|
||||
|
||||
d := getDialer()
|
||||
s, err := d.Dial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", config.MailerFromEmail.GetString())
|
||||
m.SetHeader("To", to)
|
||||
m.SetHeader("Subject", "Test from Vikunja")
|
||||
m.SetBody("text/plain", "This is a test mail! If you got this, Vikunja is correctly set up to send emails.")
|
||||
|
||||
return gomail.Send(s, m)
|
||||
}
|
||||
|
||||
// SendMail puts a mail in the queue
|
||||
func SendMail(opts *Opts) {
|
||||
m := gomail.NewMessage()
|
||||
|
|
|
@ -19,6 +19,7 @@ package metrics
|
|||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/web"
|
||||
"encoding/gob"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
@ -37,7 +38,15 @@ type ActiveUser struct {
|
|||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// ActiveUsersMap is the type used to save active users
|
||||
type ActiveUsersMap map[int64]*ActiveUser
|
||||
|
||||
// activeUsers holds a map with all active users
|
||||
var activeUsers ActiveUsersMap
|
||||
|
||||
func init() {
|
||||
activeUsers = make(ActiveUsersMap)
|
||||
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_active_users",
|
||||
Help: "The currently active users on this node",
|
||||
|
@ -57,8 +66,17 @@ func init() {
|
|||
})
|
||||
}
|
||||
|
||||
// SetUserActive sets a user as active and pushes it to redis
|
||||
func SetUserActive(a web.Auth) (err error) {
|
||||
activeUsers[a.GetID()] = &ActiveUser{
|
||||
UserID: a.GetID(),
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
return PushActiveUsers()
|
||||
}
|
||||
|
||||
// GetActiveUsers returns the active users from redis
|
||||
func GetActiveUsers() (users []*ActiveUser, err error) {
|
||||
func GetActiveUsers() (users ActiveUsersMap, err error) {
|
||||
|
||||
activeUsersR, err := r.Get(ActiveUsersKey).Bytes()
|
||||
if err != nil {
|
||||
|
@ -80,11 +98,11 @@ func GetActiveUsers() (users []*ActiveUser, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// SetActiveUsers sets the active users from redis
|
||||
func SetActiveUsers(users []*ActiveUser) (err error) {
|
||||
// PushActiveUsers pushed the content of the activeUsers map to redis
|
||||
func PushActiveUsers() (err error) {
|
||||
var b bytes.Buffer
|
||||
e := gob.NewEncoder(&b)
|
||||
if err := e.Encode(users); err != nil {
|
||||
if err := e.Encode(activeUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,11 @@ const (
|
|||
func InitMetrics() {
|
||||
r = red.GetRedis()
|
||||
|
||||
// init active users, sometimes we'll have garbage from previous runs in redis instead
|
||||
if err := PushActiveUsers(); err != nil {
|
||||
log.Fatalf("Could not set initial count for active users, error was %s", err)
|
||||
}
|
||||
|
||||
// Register total list count metric
|
||||
promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
||||
Name: "vikunja_list_count",
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"math"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type task20200420215928 struct {
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
}
|
||||
|
||||
func (s task20200420215928) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200420215928",
|
||||
Description: "Add position property to task",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(task20200420215928{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a position according to their id -> gives a starting position
|
||||
tasks := []*models.Task{}
|
||||
err = tx.Find(&tasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
task.Position = float64(task.ID) * math.Pow(2, 16)
|
||||
_, err = tx.Where("id = ?", task.ID).Update(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return tx.DropTables(task20200420215928{})
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200425182634",
|
||||
Description: "Create one bucket for each list",
|
||||
Migrate: func(tx *xorm.Engine) (err error) {
|
||||
lists := []*models.List{}
|
||||
err = tx.Find(&lists)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tasks := []*models.Task{}
|
||||
err = tx.Find(&tasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// This map contains all buckets with their list ids as key
|
||||
buckets := make(map[int64]*models.Bucket, len(lists))
|
||||
for _, l := range lists {
|
||||
buckets[l.ID] = &models.Bucket{
|
||||
ListID: l.ID,
|
||||
Title: "New Bucket",
|
||||
// The bucket creator is just the same as the list's one
|
||||
CreatedByID: l.OwnerID,
|
||||
}
|
||||
_, err = tx.Insert(buckets[l.ID])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range tasks {
|
||||
if t.ListID != l.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
t.BucketID = buckets[l.ID].ID
|
||||
_, err = tx.Where("id = ?", t.ID).Update(t)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
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-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type totp20200509103709 struct {
|
||||
Secret string `xorm:"text not null" json:"secret"`
|
||||
}
|
||||
|
||||
func (t totp20200509103709) TableName() string {
|
||||
return "totp"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200509103709",
|
||||
Description: "Fix totp secret data type",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(totp20200509103709{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return tx.DropTables(totp20200509103709{})
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type task20200515172220 struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk"`
|
||||
Title string `xorm:"varchar(250) null"`
|
||||
Text string `xorm:"varchar(250) not null"`
|
||||
}
|
||||
|
||||
func (t *task20200515172220) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
// We can't do the migration if the title column is not null but has a default value of null,
|
||||
// so we initialize it as null and change it after migrating.
|
||||
type task20200515172221 struct {
|
||||
Title string `xorm:"varchar(250) not null"`
|
||||
}
|
||||
|
||||
func (t *task20200515172221) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200515172220",
|
||||
Description: "Change task text to title",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(task20200515172220{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tasks := []*task20200515172220{}
|
||||
err = tx.Find(&tasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
task.Title = task.Text
|
||||
_, err = tx.Where("id = ?", task.ID).Update(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Sync2(task20200515172221{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sqlite is not able to drop columns. To have inserting new tasks still work, we drop the column manually.
|
||||
if config.DatabaseType.GetString() == "sqlite" {
|
||||
_, err = tx.Exec(`
|
||||
create table tasks_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
description TEXT,
|
||||
done INTEGER,
|
||||
done_at_unix INTEGER,
|
||||
due_date_unix INTEGER,
|
||||
created_by_id INTEGER not null,
|
||||
list_id INTEGER not null,
|
||||
repeat_after INTEGER,
|
||||
priority INTEGER,
|
||||
start_date_unix INTEGER,
|
||||
end_date_unix INTEGER,
|
||||
hex_color TEXT,
|
||||
percent_done REAL,
|
||||
"index" INTEGER default 0 not null,
|
||||
uid TEXT,
|
||||
created INTEGER not null,
|
||||
updated INTEGER not null,
|
||||
bucket_id INTEGER,
|
||||
position REAL,
|
||||
title TEXT
|
||||
);
|
||||
|
||||
insert into tasks_dg_tmp(id, description, done, done_at_unix, due_date_unix, created_by_id, list_id, repeat_after, priority, start_date_unix, end_date_unix, hex_color, percent_done, "index", uid, created, updated, bucket_id, position, title) select id, description, done, done_at_unix, due_date_unix, created_by_id, list_id, repeat_after, priority, start_date_unix, end_date_unix, hex_color, percent_done, "index", uid, created, updated, bucket_id, position, title from tasks;
|
||||
|
||||
drop table tasks;
|
||||
|
||||
alter table tasks_dg_tmp rename to tasks;
|
||||
|
||||
create unique index tasks_id_uindex
|
||||
on tasks (id);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
return dropTableColum(tx, "tasks", "text")
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type namespace20200515195546 struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk"`
|
||||
Title string `xorm:"varchar(250) null"`
|
||||
Name string `xorm:"varchar(250) not null"`
|
||||
}
|
||||
|
||||
func (t *namespace20200515195546) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
// We can't do the migration if the title column is not null but has a default value of null,
|
||||
// so we initialize it as null and change it after migrating.
|
||||
type namespace20200515195547 struct {
|
||||
Title string `xorm:"varchar(250) not null"`
|
||||
}
|
||||
|
||||
func (t *namespace20200515195547) TableName() string {
|
||||
return "namespaces"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200515195546",
|
||||
Description: "Change namespace name to title",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(namespace20200515195546{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
namespaces := []*namespace20200515195546{}
|
||||
err = tx.Find(&namespaces)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range namespaces {
|
||||
n.Title = n.Name
|
||||
_, err = tx.Where("id = ?", n.ID).Update(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Sync2(namespace20200515195547{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sqlite is not able to drop columns. To have inserting new namespaces still work, we drop the column manually.
|
||||
if config.DatabaseType.GetString() == "sqlite" {
|
||||
_, err = tx.Exec(`
|
||||
create table namespaces_dg_tmp
|
||||
(
|
||||
id INTEGER not null
|
||||
primary key autoincrement,
|
||||
description TEXT,
|
||||
owner_id INTEGER not null,
|
||||
created INTEGER not null,
|
||||
updated INTEGER not null,
|
||||
is_archived INTEGER default 0 not null,
|
||||
hex_color TEXT,
|
||||
title TEXT
|
||||
);
|
||||
|
||||
insert into namespaces_dg_tmp(id, description, owner_id, created, updated, is_archived, hex_color, title) select id, description, owner_id, created, updated, is_archived, hex_color, title from namespaces;
|
||||
|
||||
drop table namespaces;
|
||||
|
||||
alter table namespaces_dg_tmp rename to namespaces;
|
||||
|
||||
create unique index UQE_namespaces_id
|
||||
on namespaces (id);
|
||||
`)
|
||||
return err
|
||||
}
|
||||
|
||||
return dropTableColum(tx, "namespaces", "name")
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200516123847",
|
||||
Description: "Generate a list identifier for each list",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
lists := []*models.List{}
|
||||
err := tx.Find(&lists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, l := range lists {
|
||||
if l.Identifier != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
err := models.GenerateListIdentifier(l, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.Where("id = ?", l.ID).Update(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type unsplashPhoto20200524221534 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk"`
|
||||
FileID int64 `xorm:"not null"`
|
||||
UnsplashID string `xorm:"varchar(50)"`
|
||||
Author string `xorm:"text"`
|
||||
AuthorName string `xorm:"text"`
|
||||
}
|
||||
|
||||
func (u unsplashPhoto20200524221534) TableName() string {
|
||||
return "unsplash_photos"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200524221534",
|
||||
Description: "Create unsplash photo table",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(unsplashPhoto20200524221534{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return tx.DropTables(unsplashPhoto20200524221534{})
|
||||
},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type list20200524224611 struct {
|
||||
BackgroundFileID int64 `xorm:"null" json:"-"`
|
||||
}
|
||||
|
||||
func (s list20200524224611) TableName() string {
|
||||
return "list"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20200524224611",
|
||||
Description: "Add background file id property to list",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(list20200524224611{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return tx.DropTables(list20200524224611{})
|
||||
},
|
||||
})
|
||||
}
|
|
@ -54,7 +54,8 @@ func initMigration(x *xorm.Engine) *xormigrate.Xormigrate {
|
|||
})
|
||||
|
||||
m := xormigrate.New(x, migrations)
|
||||
m.NewLogger(log.GetLogWriter("database"))
|
||||
logger := log.NewXormLogger("")
|
||||
m.SetLogger(logger)
|
||||
m.InitSchema(initSchema)
|
||||
return m
|
||||
}
|
||||
|
|
|
@ -110,7 +110,7 @@ func (bt *BulkTask) Update() (err error) {
|
|||
}
|
||||
|
||||
_, err = sess.ID(oldtask.ID).
|
||||
Cols("text",
|
||||
Cols("title",
|
||||
"description",
|
||||
"done",
|
||||
"due_date_unix",
|
||||
|
|
|
@ -24,7 +24,7 @@ func TestBulkTask_Update(t *testing.T) {
|
|||
fields: fields{
|
||||
IDs: []int64{10, 11, 12},
|
||||
Task: Task{
|
||||
Text: "bulkupdated",
|
||||
Title: "bulkupdated",
|
||||
},
|
||||
User: &user.User{ID: 1},
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ func TestBulkTask_Update(t *testing.T) {
|
|||
fields: fields{
|
||||
IDs: []int64{10, 11, 12, 13},
|
||||
Task: Task{
|
||||
Text: "bulkupdated",
|
||||
Title: "bulkupdated",
|
||||
},
|
||||
User: &user.User{ID: 1},
|
||||
},
|
||||
|
@ -45,7 +45,7 @@ func TestBulkTask_Update(t *testing.T) {
|
|||
fields: fields{
|
||||
IDs: []int64{},
|
||||
Task: Task{
|
||||
Text: "bulkupdated",
|
||||
Title: "bulkupdated",
|
||||
},
|
||||
User: &user.User{ID: 1},
|
||||
},
|
||||
|
|
|
@ -35,7 +35,7 @@ func IsErrGenericForbidden(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrGenericForbidden) Error() string {
|
||||
return fmt.Sprintf("Forbidden")
|
||||
return "Forbidden"
|
||||
}
|
||||
|
||||
// ErrorCodeGenericForbidden holds the unique world-error code of this error
|
||||
|
@ -60,7 +60,7 @@ func IsErrIDCannotBeZero(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrIDCannotBeZero) Error() string {
|
||||
return fmt.Sprintf("ID cannot be empty or 0")
|
||||
return "ID cannot be empty or 0"
|
||||
}
|
||||
|
||||
// ErrCodeIDCannotBeZero holds the unique world-error code of this error
|
||||
|
@ -169,7 +169,7 @@ func IsErrListTitleCannotBeEmpty(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrListTitleCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("List title cannot be empty.")
|
||||
return "List title cannot be empty."
|
||||
}
|
||||
|
||||
// ErrCodeListTitleCannotBeEmpty holds the unique world-error code of this error
|
||||
|
@ -193,7 +193,7 @@ func IsErrListShareDoesNotExist(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrListShareDoesNotExist) Error() string {
|
||||
return fmt.Sprintf("List share does not exist.")
|
||||
return "List share does not exist."
|
||||
}
|
||||
|
||||
// ErrCodeListShareDoesNotExist holds the unique world-error code of this error
|
||||
|
@ -216,7 +216,7 @@ func IsErrListIdentifierIsNotUnique(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrListIdentifierIsNotUnique) Error() string {
|
||||
return fmt.Sprintf("List identifier is not unique.")
|
||||
return "List identifier is not unique."
|
||||
}
|
||||
|
||||
// ErrCodeListIdentifierIsNotUnique holds the unique world-error code of this error
|
||||
|
@ -268,7 +268,7 @@ func IsErrTaskCannotBeEmpty(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrTaskCannotBeEmpty) Error() string {
|
||||
return fmt.Sprintf("List task text cannot be empty.")
|
||||
return "List task title cannot be empty."
|
||||
}
|
||||
|
||||
// ErrCodeTaskCannotBeEmpty holds the unique world-error code of this error
|
||||
|
@ -276,7 +276,7 @@ const ErrCodeTaskCannotBeEmpty = 4001
|
|||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrTaskCannotBeEmpty) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a list task text."}
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeTaskCannotBeEmpty, Message: "You must provide at least a list task title."}
|
||||
}
|
||||
|
||||
// ErrTaskDoesNotExist represents a "ErrListDoesNotExist" kind of error. Used if the list does not exist.
|
||||
|
@ -336,7 +336,7 @@ func IsErrBulkTasksNeedAtLeastOne(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrBulkTasksNeedAtLeastOne) Error() string {
|
||||
return fmt.Sprintf("Need at least one task when bulk editing tasks")
|
||||
return "Need at least one task when bulk editing tasks"
|
||||
}
|
||||
|
||||
// ErrCodeBulkTasksNeedAtLeastOne holds the unique world-error code of this error
|
||||
|
@ -573,7 +573,7 @@ func (err ErrTaskAttachmentIsTooLarge) HTTPError() web.HTTPError {
|
|||
|
||||
// ErrInvalidSortParam represents an error where the provided sort param is invalid
|
||||
type ErrInvalidSortParam struct {
|
||||
SortBy sortProperty
|
||||
SortBy string
|
||||
}
|
||||
|
||||
// IsErrInvalidSortParam checks if an error is ErrInvalidSortParam.
|
||||
|
@ -707,6 +707,33 @@ func (err ErrInvalidTaskFilterComparator) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrInvalidTaskFilterConcatinator represents an error where the provided task field is invalid
|
||||
type ErrInvalidTaskFilterConcatinator struct {
|
||||
Concatinator taskFilterConcatinator
|
||||
}
|
||||
|
||||
// IsErrInvalidTaskFilterConcatinator checks if an error is ErrInvalidTaskFilterConcatinator.
|
||||
func IsErrInvalidTaskFilterConcatinator(err error) bool {
|
||||
_, ok := err.(ErrInvalidTaskFilterConcatinator)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidTaskFilterConcatinator) Error() string {
|
||||
return fmt.Sprintf("Task filter concatinator is invalid [Concatinator: %s]", err.Concatinator)
|
||||
}
|
||||
|
||||
// ErrCodeInvalidTaskFilterConcatinator holds the unique world-error code of this error
|
||||
const ErrCodeInvalidTaskFilterConcatinator = 4018
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrInvalidTaskFilterConcatinator) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusBadRequest,
|
||||
Code: ErrCodeInvalidTaskFilterConcatinator,
|
||||
Message: fmt.Sprintf("The task filter concatinator '%s' is invalid.", err.Concatinator),
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
@ -1251,3 +1278,31 @@ func (err ErrBucketDoesNotBelongToList) HTTPError() web.HTTPError {
|
|||
Message: "This bucket does not belong to that list.",
|
||||
}
|
||||
}
|
||||
|
||||
// ErrCannotRemoveLastBucket represents an error where a kanban bucket is the last on a list and thus cannot be removed.
|
||||
type ErrCannotRemoveLastBucket struct {
|
||||
BucketID int64
|
||||
ListID int64
|
||||
}
|
||||
|
||||
// IsErrCannotRemoveLastBucket checks if an error is ErrCannotRemoveLastBucket.
|
||||
func IsErrCannotRemoveLastBucket(err error) bool {
|
||||
_, ok := err.(ErrCannotRemoveLastBucket)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCannotRemoveLastBucket) Error() string {
|
||||
return fmt.Sprintf("Cannot remove last bucket of list [BucketID: %d, ListID: %d]", err.BucketID, err.ListID)
|
||||
}
|
||||
|
||||
// ErrCodeCannotRemoveLastBucket holds the unique world-error code of this error
|
||||
const ErrCodeCannotRemoveLastBucket = 10003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrCannotRemoveLastBucket) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusPreconditionFailed,
|
||||
Code: ErrCodeCannotRemoveLastBucket,
|
||||
Message: "You cannot remove the last bucket on this list.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/timeutil"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Bucket represents a kanban bucket
|
||||
|
@ -64,6 +64,15 @@ func getBucketByID(id int64) (b *Bucket, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func getDefaultBucket(listID int64) (bucket *Bucket, err error) {
|
||||
bucket = &Bucket{}
|
||||
_, err = x.
|
||||
Where("list_id = ?", listID).
|
||||
OrderBy("id asc").
|
||||
Get(bucket)
|
||||
return
|
||||
}
|
||||
|
||||
// ReadAll returns all buckets with their tasks for a certain list
|
||||
// @Summary Get all kanban buckets of a list
|
||||
// @Description Returns all kanban buckets with belong to a list including their tasks.
|
||||
|
@ -81,23 +90,7 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r
|
|||
// I'll probably just don't do it and instead make individual tasks archivable.
|
||||
|
||||
// Get all buckets for this list
|
||||
buckets := []*Bucket{
|
||||
{
|
||||
// This is the default bucket for all tasks which are not associated to a bucket.
|
||||
ID: 0,
|
||||
Title: "Not associated to a bucket",
|
||||
ListID: b.ListID,
|
||||
Created: timeutil.FromTime(time.Now()),
|
||||
Updated: timeutil.FromTime(time.Now()),
|
||||
CreatedByID: auth.GetID(),
|
||||
},
|
||||
}
|
||||
|
||||
buckets[0].CreatedBy, err = user.GetFromAuth(auth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buckets := []*Bucket{}
|
||||
err = x.Where("list_id = ?", b.ListID).Find(&buckets)
|
||||
if err != nil {
|
||||
return
|
||||
|
@ -123,7 +116,15 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r
|
|||
}
|
||||
|
||||
// Get all tasks for this list
|
||||
tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, &taskOptions{})
|
||||
opts := &taskOptions{
|
||||
sortby: []*sortParam{
|
||||
{
|
||||
sortBy: taskPropertyPosition,
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
},
|
||||
}
|
||||
tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, opts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -132,6 +133,11 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r
|
|||
// All tasks which are not associated to any bucket will have bucket id 0 which is the nil value for int64
|
||||
// Since we created a bucked with that id at the beginning, all tasks should be in there.
|
||||
for _, task := range tasks {
|
||||
// Check if the bucket exists in the map to prevent nil pointer panics
|
||||
if _, exists := bucketMap[task.BucketID]; !exists {
|
||||
log.Debugf("Tried to put task %d into bucket %d which does not exist in list %d", task.ID, task.BucketID, b.ListID)
|
||||
continue
|
||||
}
|
||||
bucketMap[task.BucketID].Tasks = append(bucketMap[task.BucketID].Tasks, task)
|
||||
}
|
||||
|
||||
|
@ -181,7 +187,7 @@ func (b *Bucket) Update() (err error) {
|
|||
|
||||
// Delete removes a bucket, but no tasks
|
||||
// @Summary Deletes an existing bucket
|
||||
// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks.
|
||||
// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. You cannot delete the last bucket on a list.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -193,13 +199,34 @@ func (b *Bucket) Update() (err error) {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{listID}/buckets/{bucketID} [delete]
|
||||
func (b *Bucket) Delete() (err error) {
|
||||
// Remove all associations of tasks to that bucket
|
||||
_, err = x.Where("bucket_id = ?", b.ID).Cols("bucket_id").Update(&Task{BucketID: 0})
|
||||
// Prevent removing the last bucket
|
||||
total, err := x.Where("list_id = ?", b.ListID).Count(&Bucket{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if total <= 1 {
|
||||
return ErrCannotRemoveLastBucket{
|
||||
BucketID: b.ID,
|
||||
ListID: b.ListID,
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the bucket itself
|
||||
_, err = x.Where("id = ?", b.ID).Delete(&Bucket{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the default bucket
|
||||
defaultBucket, err := getDefaultBucket(b.ListID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove all associations of tasks to that bucket
|
||||
_, err = x.Where("bucket_id = ?", b.ID).Cols("bucket_id").Update(&Task{BucketID: defaultBucket.ID})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -38,30 +38,55 @@ func TestBucket_ReadAll(t *testing.T) {
|
|||
assert.Equal(t, testuser.ID, buckets[0].CreatedBy.ID)
|
||||
assert.Equal(t, testuser.ID, buckets[1].CreatedBy.ID)
|
||||
assert.Equal(t, testuser.ID, buckets[2].CreatedBy.ID)
|
||||
assert.Equal(t, testuser.ID, buckets[3].CreatedBy.ID)
|
||||
|
||||
// Assert our three test buckets + one for all tasks without a bucket
|
||||
assert.Len(t, buckets, 4)
|
||||
// Assert our three test buckets
|
||||
assert.Len(t, buckets, 3)
|
||||
|
||||
// Assert all tasks are in the right bucket
|
||||
assert.Len(t, buckets[0].Tasks, 10)
|
||||
assert.Len(t, buckets[1].Tasks, 2)
|
||||
assert.Len(t, buckets[0].Tasks, 12)
|
||||
assert.Len(t, buckets[1].Tasks, 3)
|
||||
assert.Len(t, buckets[2].Tasks, 3)
|
||||
assert.Len(t, buckets[3].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[1].ID)
|
||||
assert.Equal(t, int64(2), buckets[2].ID)
|
||||
assert.Equal(t, int64(3), buckets[3].ID)
|
||||
assert.Equal(t, int64(1), buckets[0].ID)
|
||||
assert.Equal(t, int64(2), buckets[1].ID)
|
||||
assert.Equal(t, int64(3), buckets[2].ID)
|
||||
|
||||
// Kinda assert all tasks are in the right buckets
|
||||
assert.Equal(t, int64(0), buckets[0].Tasks[0].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[2].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[2].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[2].Tasks[2].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[3].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[3].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[3].Tasks[2].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[0].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(1), buckets[0].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(2), buckets[1].Tasks[2].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[0].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[1].BucketID)
|
||||
assert.Equal(t, int64(3), buckets[2].Tasks[2].BucketID)
|
||||
}
|
||||
|
||||
func TestBucket_Delete(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
b := &Bucket{
|
||||
ID: 2, // The second bucket only has 3 tasks
|
||||
ListID: 1,
|
||||
}
|
||||
err := b.Delete()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Assert all tasks have been moved to bucket 1 as that one is the first
|
||||
tasks := []*Task{}
|
||||
err = x.Where("bucket_id = ?", 1).Find(&tasks)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tasks, 15)
|
||||
})
|
||||
t.Run("last bucket in list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
b := &Bucket{
|
||||
ID: 34,
|
||||
ListID: 18,
|
||||
}
|
||||
err := b.Delete()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrCannotRemoveLastBucket(err))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -209,15 +209,11 @@ func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, res
|
|||
}
|
||||
|
||||
// Get the total number of entries
|
||||
condCount := builder.And(builder.In("label_task.task_id", opts.TaskIDs), builder.NotNull{"label_task.label_id"})
|
||||
if opts.GetUnusedLabels {
|
||||
condCount = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.ID})
|
||||
}
|
||||
totalEntries, err = x.Table("labels").
|
||||
Select("count(DISTINCT labels.id)").
|
||||
Join("LEFT", "label_task", "label_task.label_id = labels.id").
|
||||
Where(condCount).
|
||||
Where(cond).
|
||||
And("labels.title LIKE ?", "%"+opts.Search+"%").
|
||||
GroupBy(groupBy).
|
||||
Count(&Label{})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
|
|
|
@ -99,10 +99,16 @@ func GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{list}/shares [put]
|
||||
func (share *LinkSharing) Create(a web.Auth) (err error) {
|
||||
|
||||
err = share.Right.isValid()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
share.SharedByID = a.GetID()
|
||||
share.Hash = utils.MakeRandomString(40)
|
||||
_, err = x.Insert(share)
|
||||
share.SharedBy, _ = a.(*user.User)
|
||||
share.SharedBy, _ = user.GetFromAuth(a)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -53,9 +53,16 @@ func (share *LinkSharing) canDoLinkShare(a web.Auth) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
l, err := GetListSimplByTaskID(share.ListID)
|
||||
l := &List{ID: share.ListID}
|
||||
err := l.GetSimpleByID()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if the user is admin when the link right is admin
|
||||
if share.Right == RightAdmin {
|
||||
return l.IsAdmin(a)
|
||||
}
|
||||
|
||||
return l.CanWrite(a)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,14 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/timeutil"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web"
|
||||
"strings"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// List represents a list of tasks
|
||||
|
@ -49,6 +52,11 @@ type List struct {
|
|||
// Whether or not a list is archived.
|
||||
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
|
||||
|
||||
// The id of the file this list has set as background
|
||||
BackgroundFileID int64 `xorm:"null" json:"-"`
|
||||
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background
|
||||
BackgroundInformation interface{} `xorm:"-" json:"background_information"`
|
||||
|
||||
// A timestamp when this list was created. You cannot change this value.
|
||||
Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this list was last updated. You cannot change this value.
|
||||
|
@ -164,6 +172,16 @@ func (l *List) ReadOne() (err error) {
|
|||
l.IsArchived = true
|
||||
}
|
||||
}
|
||||
|
||||
// Get any background information if there is one set
|
||||
if l.BackgroundFileID != 0 {
|
||||
// Currently unsplash only
|
||||
l.BackgroundInformation, err = GetUnsplashPhotoByFileID(l.BackgroundFileID)
|
||||
if err != nil && !files.IsErrFileIsNotUnsplashFile(err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -351,6 +369,43 @@ func (l *List) CheckIsArchived() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GenerateListIdentifier generates a unique random list identifier based on the list title.
|
||||
// If it is not able to find one, the list identifier will be empty.
|
||||
func GenerateListIdentifier(l *List, sess *xorm.Engine) (err error) {
|
||||
|
||||
// The general idea here is to take the title and slice it into pieces, until we found a unique piece.
|
||||
|
||||
var exists = true
|
||||
titleSlug := strings.Replace(strings.ToUpper(l.Title), " ", "", -1)
|
||||
|
||||
// We can save at most 10 characters in the db, so we need to ensure it has at most 10 characters
|
||||
if len(titleSlug) > 10 {
|
||||
titleSlug = titleSlug[0:9]
|
||||
}
|
||||
|
||||
var i = 0
|
||||
|
||||
for exists {
|
||||
|
||||
// Prevent endless looping
|
||||
if i == len(titleSlug) {
|
||||
break
|
||||
}
|
||||
|
||||
// Take a random part of the title slug, starting at the beginning
|
||||
l.Identifier = titleSlug[i:]
|
||||
exists, err = sess.
|
||||
Where("identifier = ?", l.Identifier).
|
||||
And("id != ?", l.ID).
|
||||
Exist(&List{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
i++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateOrUpdateList updates a list or creates it if it doesn't exist
|
||||
func CreateOrUpdateList(list *List) (err error) {
|
||||
|
||||
|
@ -376,6 +431,14 @@ func CreateOrUpdateList(list *List) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Generate a random list identifier base on the list title
|
||||
if list.ID == 0 && list.Identifier == "" {
|
||||
err = GenerateListIdentifier(list, x)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if list.ID == 0 {
|
||||
_, err = x.Insert(list)
|
||||
metrics.UpdateCount(1, metrics.ListCountKey)
|
||||
|
@ -384,13 +447,11 @@ func CreateOrUpdateList(list *List) (err error) {
|
|||
colsToUpdate := []string{
|
||||
"title",
|
||||
"is_archived",
|
||||
"identifier",
|
||||
}
|
||||
if list.Description != "" {
|
||||
colsToUpdate = append(colsToUpdate, "description")
|
||||
}
|
||||
if list.Identifier != "" {
|
||||
colsToUpdate = append(colsToUpdate, "identifier")
|
||||
}
|
||||
if list.HexColor != "" {
|
||||
colsToUpdate = append(colsToUpdate, "hex_color")
|
||||
}
|
||||
|
@ -477,7 +538,17 @@ func (l *List) Create(a web.Auth) (err error) {
|
|||
l.Owner = doer
|
||||
l.ID = 0 // Otherwise only the first time a new list would be created
|
||||
|
||||
return CreateOrUpdateList(l)
|
||||
err = CreateOrUpdateList(l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new first bucket for this list
|
||||
b := &Bucket{
|
||||
ListID: l.ID,
|
||||
Title: "New Bucket",
|
||||
}
|
||||
return b.Create(a)
|
||||
}
|
||||
|
||||
// Delete implements the delete method of CRUDable
|
||||
|
@ -505,3 +576,16 @@ func (l *List) Delete() (err error) {
|
|||
_, err = x.Where("list_id = ?", l.ID).Delete(&Task{})
|
||||
return
|
||||
}
|
||||
|
||||
// SetListBackground sets a background file as list background in the db
|
||||
func SetListBackground(listID int64, background *files.File) (err error) {
|
||||
l := &List{
|
||||
ID: listID,
|
||||
BackgroundFileID: background.ID,
|
||||
}
|
||||
_, err = x.
|
||||
Where("id = ?", l.ID).
|
||||
Cols("background_file_id").
|
||||
Update(l)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -235,11 +235,11 @@ func (lu *ListUser) Update() (err error) {
|
|||
}
|
||||
|
||||
// Check if the user exists
|
||||
user, err := user.GetUserByUsername(lu.Username)
|
||||
u, err := user.GetUserByUsername(lu.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lu.UserID = user.ID
|
||||
lu.UserID = u.ID
|
||||
|
||||
_, err = x.
|
||||
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
||||
|
|
|
@ -31,7 +31,7 @@ type Namespace struct {
|
|||
// The unique, numeric id of this namespace.
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"namespace"`
|
||||
// The name of this namespace.
|
||||
Name string `xorm:"varchar(250) not null" json:"name" valid:"required,runelength(5|250)" minLength:"5" maxLength:"250"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"required,runelength(5|250)" minLength:"5" maxLength:"250"`
|
||||
// The description of the namespace
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
OwnerID int64 `xorm:"int(11) not null INDEX" json:"-"`
|
||||
|
@ -57,7 +57,7 @@ type Namespace struct {
|
|||
// PseudoNamespace is a pseudo namespace used to hold shared lists
|
||||
var PseudoNamespace = Namespace{
|
||||
ID: -1,
|
||||
Name: "Shared Lists",
|
||||
Title: "Shared Lists",
|
||||
Description: "Lists of other users shared with you via teams or directly.",
|
||||
Created: timeutil.FromTime(time.Now()),
|
||||
Updated: timeutil.FromTime(time.Now()),
|
||||
|
@ -201,7 +201,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
Or("users_namespace.user_id = ?", doer.ID).
|
||||
GroupBy("namespaces.id").
|
||||
Where("namespaces.name LIKE ?", "%"+search+"%").
|
||||
Where("namespaces.title LIKE ?", "%"+search+"%").
|
||||
Where(isArchivedCond)
|
||||
if limit > 0 {
|
||||
query = query.Limit(limit, start)
|
||||
|
@ -211,30 +211,19 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
return all, 0, 0, err
|
||||
}
|
||||
|
||||
// Get all users
|
||||
users := []*user.User{}
|
||||
err = x.Select("users.*").
|
||||
Table("namespaces").
|
||||
Join("LEFT", "team_namespaces", "namespaces.id = team_namespaces.namespace_id").
|
||||
Join("LEFT", "team_members", "team_members.team_id = team_namespaces.team_id").
|
||||
Join("LEFT", "users_namespace", "users_namespace.namespace_id = namespaces.id").
|
||||
Join("INNER", "users", "users.id = namespaces.owner_id").
|
||||
Where("team_members.user_id = ?", doer.ID).
|
||||
Or("namespaces.owner_id = ?", doer.ID).
|
||||
Or("users_namespace.user_id = ?", doer.ID).
|
||||
And("namespaces.is_archived = ?", n.IsArchived).
|
||||
Where(isArchivedCond).
|
||||
GroupBy("users.id").
|
||||
Find(&users)
|
||||
|
||||
if err != nil {
|
||||
return all, 0, 0, err
|
||||
}
|
||||
|
||||
// Make a list of namespace ids
|
||||
var namespaceids []int64
|
||||
var userIDs []int64
|
||||
for _, nsp := range all {
|
||||
namespaceids = append(namespaceids, nsp.ID)
|
||||
userIDs = append(userIDs, nsp.OwnerID)
|
||||
}
|
||||
|
||||
// Get all owners
|
||||
userMap := make(map[int64]*user.User)
|
||||
err = x.In("id", userIDs).Find(&userMap)
|
||||
if err != nil {
|
||||
return all, 0, 0, err
|
||||
}
|
||||
|
||||
// Get all lists
|
||||
|
@ -287,24 +276,19 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
nMap := make(map[int64]*NamespaceWithLists, len(all))
|
||||
|
||||
// Put objects in our namespace list
|
||||
// TODO: Refactor this to use maps for better efficiency
|
||||
for i, n := range all {
|
||||
for _, n := range all {
|
||||
|
||||
// Users
|
||||
for _, u := range users {
|
||||
if n.OwnerID == u.ID {
|
||||
all[i].Owner = u
|
||||
break
|
||||
}
|
||||
}
|
||||
n.Owner = userMap[n.OwnerID]
|
||||
|
||||
// List infos
|
||||
for _, l := range lists {
|
||||
if n.ID == l.NamespaceID {
|
||||
all[i].Lists = append(all[i].Lists, l)
|
||||
}
|
||||
}
|
||||
nMap[n.ID] = n
|
||||
}
|
||||
|
||||
for _, list := range lists {
|
||||
nMap[list.NamespaceID].Lists = append(nMap[list.NamespaceID].Lists, list)
|
||||
}
|
||||
|
||||
numberOfTotalItems, err = x.
|
||||
|
@ -317,7 +301,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
Or("users_namespace.user_id = ?", doer.ID).
|
||||
And("namespaces.is_archived = false").
|
||||
GroupBy("namespaces.id").
|
||||
Where("namespaces.name LIKE ?", "%"+search+"%").
|
||||
Where("namespaces.title LIKE ?", "%"+search+"%").
|
||||
Count(&NamespaceWithLists{})
|
||||
if err != nil {
|
||||
return all, 0, 0, err
|
||||
|
@ -341,7 +325,7 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
|
|||
// @Router /namespaces [put]
|
||||
func (n *Namespace) Create(a web.Auth) (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Name == "" {
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: 0, UserID: a.GetID()}
|
||||
}
|
||||
n.ID = 0 // This would otherwise prevent the creation of new lists after one was created
|
||||
|
@ -434,7 +418,7 @@ func (n *Namespace) Delete() (err error) {
|
|||
// @Router /namespace/{id} [post]
|
||||
func (n *Namespace) Update() (err error) {
|
||||
// Check if we have at least a name
|
||||
if n.Name == "" {
|
||||
if n.Title == "" {
|
||||
return ErrNamespaceNameCannotBeEmpty{NamespaceID: n.ID}
|
||||
}
|
||||
|
||||
|
|
|
@ -220,7 +220,7 @@ func (tn *TeamNamespace) Update() (err error) {
|
|||
}
|
||||
|
||||
_, err = x.
|
||||
Where("namespace_id = ? AND team_id = ?", tn.TeamID, tn.TeamID).
|
||||
Where("namespace_id = ? AND team_id = ?", tn.NamespaceID, tn.TeamID).
|
||||
Cols("right").
|
||||
Update(tn)
|
||||
return
|
||||
|
|
|
@ -32,7 +32,7 @@ func TestNamespace_Create(t *testing.T) {
|
|||
|
||||
// Dummy namespace
|
||||
dummynamespace := Namespace{
|
||||
Name: "Test",
|
||||
Title: "Test",
|
||||
Description: "Lorem Ipsum",
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ func TestNamespace_Create(t *testing.T) {
|
|||
assert.True(t, allowed)
|
||||
err = dummynamespace.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, dummynamespace.Name, "Test")
|
||||
assert.Equal(t, dummynamespace.Title, "Test")
|
||||
|
||||
// Try creating one without a name
|
||||
n2 := Namespace{}
|
||||
|
@ -92,13 +92,13 @@ func TestNamespace_Create(t *testing.T) {
|
|||
assert.True(t, user.IsErrUserDoesNotExist(err))
|
||||
|
||||
// Try updating without a name
|
||||
dummynamespace.Name = ""
|
||||
dummynamespace.Title = ""
|
||||
err = dummynamespace.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceNameCannotBeEmpty(err))
|
||||
|
||||
// Try updating a nonexistant one
|
||||
n := Namespace{ID: 284729, Name: "Lorem"}
|
||||
n := Namespace{ID: 284729, Title: "Lorem"}
|
||||
err = n.Update()
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrNamespaceDoesNotExist(err))
|
||||
|
|
|
@ -43,6 +43,8 @@ type TaskCollection struct {
|
|||
// The comparator for field and value
|
||||
FilterComparator []string `query:"filter_comparator"`
|
||||
FilterComparatorArr []string `query:"filter_comparator[]"`
|
||||
// The way all filter conditions are concatenated together, can be either "and" or "or".,
|
||||
FilterConcat string `query:"filter_concat"`
|
||||
|
||||
web.CRUDable `xorm:"-" json:"-"`
|
||||
web.Rights `xorm:"-" json:"-"`
|
||||
|
@ -52,7 +54,7 @@ func validateTaskField(fieldName string) error {
|
|||
switch fieldName {
|
||||
case
|
||||
taskPropertyID,
|
||||
taskPropertyText,
|
||||
taskPropertyTitle,
|
||||
taskPropertyDescription,
|
||||
taskPropertyDone,
|
||||
taskPropertyDoneAtUnix,
|
||||
|
@ -67,7 +69,8 @@ func validateTaskField(fieldName string) error {
|
|||
taskPropertyPercentDone,
|
||||
taskPropertyUID,
|
||||
taskPropertyCreated,
|
||||
taskPropertyUpdated:
|
||||
taskPropertyUpdated,
|
||||
taskPropertyPosition:
|
||||
return nil
|
||||
}
|
||||
return ErrInvalidTaskField{TaskField: fieldName}
|
||||
|
@ -84,11 +87,12 @@ func validateTaskField(fieldName string) error {
|
|||
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
|
||||
// @Param s query string false "Search tasks by task text."
|
||||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `text`, `description`, `done`, `done_at_unix`, `due_date_unix`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date_unix`, `end_date_unix`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at_unix`, `due_date_unix`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date_unix`, `end_date_unix`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
|
||||
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
|
||||
// @Param filter_by query string false "The name of the field to filter by. Accepts an array for multiple filters which will be chanied together, all supplied filter must match."
|
||||
// @Param filter_value query string false "The value to filter for."
|
||||
// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less` and `less_equals`. Defaults to `equals`"
|
||||
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Task "The tasks"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
@ -106,7 +110,7 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
|
|||
var sort = make([]*sortParam, 0, len(tf.SortBy))
|
||||
for i, s := range tf.SortBy {
|
||||
param := &sortParam{
|
||||
sortBy: sortProperty(s),
|
||||
sortBy: s,
|
||||
orderBy: orderAscending,
|
||||
}
|
||||
// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
|
||||
|
@ -114,6 +118,20 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
|
|||
if len(tf.OrderBy) > i {
|
||||
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
|
||||
}
|
||||
|
||||
// Special case for pseudo date fields
|
||||
// FIXME: This is really dirty, to fix this properly the db fields should be renamed
|
||||
switch param.sortBy {
|
||||
case "done_at":
|
||||
param.sortBy = taskPropertyDoneAtUnix
|
||||
case "due_date":
|
||||
param.sortBy = taskPropertyDueDateUnix
|
||||
case "start_date":
|
||||
param.sortBy = taskPropertyStartDateUnix
|
||||
case "end_date":
|
||||
param.sortBy = taskPropertyEndDateUnix
|
||||
}
|
||||
|
||||
// Param validation
|
||||
if err := param.validate(); err != nil {
|
||||
return nil, 0, 0, err
|
||||
|
@ -122,10 +140,11 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
|
|||
}
|
||||
|
||||
taskopts := &taskOptions{
|
||||
search: search,
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
sortby: sort,
|
||||
search: search,
|
||||
page: page,
|
||||
perPage: perPage,
|
||||
sortby: sort,
|
||||
filterConcat: taskFilterConcatinator(tf.FilterConcat),
|
||||
}
|
||||
|
||||
taskopts.filters, err = getTaskFiltersByCollections(tf)
|
||||
|
|
|
@ -53,7 +53,13 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
|
|||
}
|
||||
|
||||
if len(c.FilterComparatorArr) > 0 {
|
||||
c.FilterValue = append(c.FilterValue, c.FilterComparatorArr...)
|
||||
c.FilterComparator = append(c.FilterComparator, c.FilterComparatorArr...)
|
||||
}
|
||||
|
||||
if c.FilterConcat != "" && c.FilterConcat != filterConcatAnd && c.FilterConcat != filterConcatOr {
|
||||
return nil, ErrInvalidTaskFilterConcatinator{
|
||||
Concatinator: taskFilterConcatinator(c.FilterConcat),
|
||||
}
|
||||
}
|
||||
|
||||
filters = make([]*taskFilter, 0, len(c.FilterBy))
|
||||
|
@ -84,7 +90,7 @@ func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err
|
|||
}
|
||||
|
||||
// Special case for pseudo date fields
|
||||
// FIXME: This is really dirty, to fix this the db fields should be renamed
|
||||
// FIXME: This is really dirty, to fix this properly the db fields should be renamed
|
||||
if filter.field+"_unix" == taskPropertyDoneAtUnix ||
|
||||
filter.field+"_unix" == taskPropertyDueDateUnix ||
|
||||
filter.field+"_unix" == taskPropertyStartDateUnix ||
|
||||
|
|
|
@ -16,27 +16,18 @@
|
|||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/timeutil"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type (
|
||||
sortParam struct {
|
||||
sortBy sortProperty
|
||||
sortBy string
|
||||
orderBy sortOrder // asc or desc
|
||||
}
|
||||
|
||||
sortProperty string
|
||||
|
||||
sortOrder string
|
||||
)
|
||||
|
||||
const (
|
||||
taskPropertyID string = "id"
|
||||
taskPropertyText string = "text"
|
||||
taskPropertyTitle string = "title"
|
||||
taskPropertyDescription string = "description"
|
||||
taskPropertyDone string = "done"
|
||||
taskPropertyDoneAtUnix string = "done_at_unix"
|
||||
|
@ -52,12 +43,9 @@ const (
|
|||
taskPropertyUID string = "uid"
|
||||
taskPropertyCreated string = "created"
|
||||
taskPropertyUpdated string = "updated"
|
||||
taskPropertyPosition string = "position"
|
||||
)
|
||||
|
||||
func (p sortProperty) String() string {
|
||||
return string(p)
|
||||
}
|
||||
|
||||
const (
|
||||
orderInvalid sortOrder = "invalid"
|
||||
orderAscending sortOrder = "asc"
|
||||
|
@ -82,139 +70,5 @@ func (sp *sortParam) validate() error {
|
|||
if sp.orderBy != orderDescending && sp.orderBy != orderAscending {
|
||||
return ErrInvalidSortOrder{OrderBy: sp.orderBy}
|
||||
}
|
||||
return validateTaskField(string(sp.sortBy))
|
||||
}
|
||||
|
||||
type taskComparator func(lhs, rhs *Task) int64
|
||||
|
||||
func mustMakeComparator(fieldName string) taskComparator {
|
||||
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(fieldName)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Field '%s' has not been found on Task", fieldName))
|
||||
}
|
||||
|
||||
extractProp := func(task *Task) interface{} {
|
||||
return reflect.ValueOf(task).Elem().FieldByIndex(field.Index).Interface()
|
||||
}
|
||||
|
||||
// Special case for handling TimeStamp types
|
||||
if field.Type.Name() == "TimeStamp" {
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
return int64(extractProp(lhs).(timeutil.TimeStamp)) - int64(extractProp(rhs).(timeutil.TimeStamp))
|
||||
}
|
||||
}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Int64:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
return extractProp(lhs).(int64) - extractProp(rhs).(int64)
|
||||
}
|
||||
case reflect.Float64:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
floatLHS, floatRHS := extractProp(lhs).(float64), extractProp(rhs).(float64)
|
||||
if floatLHS > floatRHS {
|
||||
return 1
|
||||
} else if floatLHS < floatRHS {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
case reflect.String:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
strLHS, strRHS := extractProp(lhs).(string), extractProp(rhs).(string)
|
||||
if strLHS > strRHS {
|
||||
return 1
|
||||
} else if strLHS < strRHS {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
case reflect.Bool:
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
boolLHS, boolRHS := extractProp(lhs).(bool), extractProp(rhs).(bool)
|
||||
if !boolLHS && boolRHS {
|
||||
return -1
|
||||
} else if boolLHS && !boolRHS {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("Unsupported type for sorting: %s", field.Type.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
// This is a map of properties that can be sorted by
|
||||
// and their appropriate comparator function.
|
||||
// The comparator function sorts in ascending mode.
|
||||
var propertyComparators = map[string]taskComparator{
|
||||
taskPropertyID: mustMakeComparator("ID"),
|
||||
taskPropertyText: mustMakeComparator("Text"),
|
||||
taskPropertyDescription: mustMakeComparator("Description"),
|
||||
taskPropertyDone: mustMakeComparator("Done"),
|
||||
taskPropertyDoneAtUnix: mustMakeComparator("DoneAt"),
|
||||
taskPropertyDueDateUnix: mustMakeComparator("DueDate"),
|
||||
taskPropertyCreatedByID: mustMakeComparator("CreatedByID"),
|
||||
taskPropertyListID: mustMakeComparator("ListID"),
|
||||
taskPropertyRepeatAfter: mustMakeComparator("RepeatAfter"),
|
||||
taskPropertyPriority: mustMakeComparator("Priority"),
|
||||
taskPropertyStartDateUnix: mustMakeComparator("StartDate"),
|
||||
taskPropertyEndDateUnix: mustMakeComparator("EndDate"),
|
||||
taskPropertyHexColor: mustMakeComparator("HexColor"),
|
||||
taskPropertyPercentDone: mustMakeComparator("PercentDone"),
|
||||
taskPropertyUID: mustMakeComparator("UID"),
|
||||
taskPropertyCreated: mustMakeComparator("Created"),
|
||||
taskPropertyUpdated: mustMakeComparator("Updated"),
|
||||
}
|
||||
|
||||
// Creates a taskComparator that sorts by the first comparator and falls back to
|
||||
// the second one (and so on...) if the properties were equal.
|
||||
func combineComparators(comparators ...taskComparator) taskComparator {
|
||||
return func(lhs, rhs *Task) int64 {
|
||||
for _, compare := range comparators {
|
||||
res := compare(lhs, rhs)
|
||||
if res != 0 {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func sortTasks(tasks []*Task, by []*sortParam) {
|
||||
|
||||
// Always sort at least by id asc so we have a consistent order of items every time
|
||||
// If we would not do this, we would get a different order for items with the same content every time
|
||||
// the slice is sorted. To circumvent this, we always order at least by ID.
|
||||
if len(by) == 0 ||
|
||||
(len(by) > 0 && by[len(by)-1].sortBy != sortProperty(taskPropertyID)) { // Don't sort by ID last if the id parameter is already passed as the last parameter.
|
||||
by = append(by, &sortParam{sortBy: sortProperty(taskPropertyID), orderBy: orderAscending})
|
||||
}
|
||||
|
||||
comparators := make([]taskComparator, 0, len(by))
|
||||
for _, param := range by {
|
||||
comparator, ok := propertyComparators[string(param.sortBy)]
|
||||
if !ok {
|
||||
panic("No suitable comparator for sortBy found! Param was " + param.sortBy)
|
||||
}
|
||||
|
||||
// This is a descending sort, so we need to negate the comparator (i.e. switch the inputs).
|
||||
if param.orderBy == orderDescending {
|
||||
oldComparator := comparator
|
||||
comparator = func(lhs, rhs *Task) int64 {
|
||||
return oldComparator(lhs, rhs) * -1
|
||||
}
|
||||
}
|
||||
|
||||
comparators = append(comparators, comparator)
|
||||
}
|
||||
|
||||
combinedComparator := combineComparators(comparators...)
|
||||
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
lhs, rhs := tasks[i], tasks[j]
|
||||
|
||||
res := combinedComparator(lhs, rhs)
|
||||
return res <= 0
|
||||
})
|
||||
return validateTaskField(sp.sortBy)
|
||||
}
|
||||
|
|
|
@ -17,10 +17,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -46,7 +43,7 @@ func TestSortParamValidation(t *testing.T) {
|
|||
t.Run("Test valid sort by", func(t *testing.T) {
|
||||
for _, test := range []string{
|
||||
taskPropertyID,
|
||||
taskPropertyText,
|
||||
taskPropertyTitle,
|
||||
taskPropertyDescription,
|
||||
taskPropertyDone,
|
||||
taskPropertyDoneAtUnix,
|
||||
|
@ -62,11 +59,12 @@ func TestSortParamValidation(t *testing.T) {
|
|||
taskPropertyUID,
|
||||
taskPropertyCreated,
|
||||
taskPropertyUpdated,
|
||||
taskPropertyPosition,
|
||||
} {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
s := &sortParam{
|
||||
orderBy: orderAscending,
|
||||
sortBy: sortProperty(test),
|
||||
sortBy: test,
|
||||
}
|
||||
err := s.validate()
|
||||
assert.NoError(t, err)
|
||||
|
@ -92,774 +90,3 @@ func TestSortParamValidation(t *testing.T) {
|
|||
assert.True(t, IsErrInvalidTaskField(err))
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
task1 = &Task{
|
||||
ID: 1,
|
||||
Text: "aaa",
|
||||
Description: "Lorem Ipsum",
|
||||
Done: true,
|
||||
DoneAt: 1543626000,
|
||||
ListID: 1,
|
||||
UID: "JywtBPCESImlyKugvaZWrxmXAFAWXFISMeXYImEh",
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task2 = &Task{
|
||||
ID: 2,
|
||||
Text: "bbb",
|
||||
Description: "Arem Ipsum",
|
||||
Done: true,
|
||||
DoneAt: 1543626724,
|
||||
CreatedByID: 1,
|
||||
ListID: 2,
|
||||
PercentDone: 0.3,
|
||||
StartDate: 1543626724,
|
||||
Created: 1553626724,
|
||||
Updated: 1553626724,
|
||||
}
|
||||
task3 = &Task{
|
||||
ID: 3,
|
||||
Text: "ccc",
|
||||
DueDate: 1583626724,
|
||||
Priority: 100,
|
||||
ListID: 3,
|
||||
HexColor: "000000",
|
||||
PercentDone: 0.1,
|
||||
Updated: 1555555555,
|
||||
}
|
||||
task4 = &Task{
|
||||
ID: 4,
|
||||
Text: "ddd",
|
||||
Priority: 1,
|
||||
StartDate: 1643626724,
|
||||
ListID: 1,
|
||||
}
|
||||
task5 = &Task{
|
||||
ID: 5,
|
||||
Text: "eef",
|
||||
Priority: 50,
|
||||
UID: "shggzCHQWLhGNMNsOGOCOjcVkInOYjTAnORqTkdL",
|
||||
DueDate: 1543636724,
|
||||
Updated: 1565555555,
|
||||
}
|
||||
task6 = &Task{
|
||||
ID: 6,
|
||||
Text: "eef",
|
||||
DueDate: 1543616724,
|
||||
RepeatAfter: 6400,
|
||||
CreatedByID: 2,
|
||||
HexColor: "ffffff",
|
||||
}
|
||||
task7 = &Task{
|
||||
ID: 7,
|
||||
Text: "mmmn",
|
||||
Description: "Zoremis",
|
||||
StartDate: 1544600000,
|
||||
EndDate: 1584600000,
|
||||
UID: "tyzCZuLMSKhwclJOsDyDcUdyVAPBDOPHNTBOLTcW",
|
||||
}
|
||||
task8 = &Task{
|
||||
ID: 8,
|
||||
Text: "b123",
|
||||
EndDate: 1544700000,
|
||||
}
|
||||
task9 = &Task{
|
||||
ID: 9,
|
||||
Done: true,
|
||||
DoneAt: 1573626724,
|
||||
Text: "a123",
|
||||
RepeatAfter: 86000,
|
||||
StartDate: 1544600000,
|
||||
EndDate: 1544700000,
|
||||
}
|
||||
task10 = &Task{
|
||||
ID: 10,
|
||||
Text: "zzz",
|
||||
Priority: 10,
|
||||
PercentDone: 1,
|
||||
}
|
||||
)
|
||||
|
||||
type taskSortTestCase struct {
|
||||
name string
|
||||
wantAsc []*Task
|
||||
wantDesc []*Task
|
||||
sortProperty string
|
||||
}
|
||||
|
||||
var taskSortTestCases = []taskSortTestCase{
|
||||
{
|
||||
name: "id",
|
||||
sortProperty: taskPropertyID,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task10,
|
||||
task9,
|
||||
task8,
|
||||
task7,
|
||||
task6,
|
||||
task5,
|
||||
task4,
|
||||
task3,
|
||||
task2,
|
||||
task1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
sortProperty: taskPropertyText,
|
||||
wantAsc: []*Task{
|
||||
task9,
|
||||
task1,
|
||||
task8,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task10,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task10,
|
||||
task7,
|
||||
task5,
|
||||
task6,
|
||||
task4,
|
||||
task3,
|
||||
task2,
|
||||
task8,
|
||||
task1,
|
||||
task9,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
sortProperty: taskPropertyDescription,
|
||||
wantAsc: []*Task{
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task2,
|
||||
task1,
|
||||
task7,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task7,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "done",
|
||||
sortProperty: taskPropertyDone,
|
||||
wantAsc: []*Task{
|
||||
// These are not
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
// These are done
|
||||
task1,
|
||||
task2,
|
||||
task9,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
// These are done
|
||||
task1,
|
||||
task2,
|
||||
task9,
|
||||
// These are not
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "done at",
|
||||
sortProperty: taskPropertyDoneAtUnix,
|
||||
wantAsc: []*Task{
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
task1,
|
||||
task2,
|
||||
task9,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task9,
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "due date",
|
||||
sortProperty: taskPropertyDueDateUnix,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task6,
|
||||
task5,
|
||||
task3,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task3,
|
||||
task5,
|
||||
task6,
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "created by id",
|
||||
sortProperty: taskPropertyCreatedByID,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task2,
|
||||
task6,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task6,
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list id",
|
||||
sortProperty: taskPropertyListID,
|
||||
wantAsc: []*Task{
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task4,
|
||||
task2,
|
||||
task3,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task3,
|
||||
task2,
|
||||
task1,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "repeat after",
|
||||
sortProperty: taskPropertyRepeatAfter,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
task6,
|
||||
task9,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task9,
|
||||
task6,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "priority",
|
||||
sortProperty: taskPropertyPriority,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task4,
|
||||
task10,
|
||||
task5,
|
||||
task3,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task3,
|
||||
task5,
|
||||
task10,
|
||||
task4,
|
||||
task1,
|
||||
task2,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start date",
|
||||
sortProperty: taskPropertyStartDateUnix,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task3,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task10,
|
||||
task2,
|
||||
task7,
|
||||
task9,
|
||||
task4,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task4,
|
||||
task7,
|
||||
task9,
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task5,
|
||||
task6,
|
||||
task8,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "end date",
|
||||
sortProperty: taskPropertyEndDateUnix,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task10,
|
||||
task8,
|
||||
task9,
|
||||
task7,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hex color",
|
||||
sortProperty: taskPropertyHexColor,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task3,
|
||||
task6,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task6,
|
||||
task3,
|
||||
task1,
|
||||
task2,
|
||||
task4,
|
||||
task5,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "percent done",
|
||||
sortProperty: taskPropertyPercentDone,
|
||||
wantAsc: []*Task{
|
||||
task1,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task3,
|
||||
task2,
|
||||
task10,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task10,
|
||||
task2,
|
||||
task3,
|
||||
task1,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "uid",
|
||||
sortProperty: taskPropertyUID,
|
||||
wantAsc: []*Task{
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task5,
|
||||
task7,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task7,
|
||||
task5,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task4,
|
||||
task6,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "created",
|
||||
sortProperty: taskPropertyCreated,
|
||||
wantAsc: []*Task{
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task2,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task2,
|
||||
task1,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "updated",
|
||||
sortProperty: taskPropertyUpdated,
|
||||
wantAsc: []*Task{
|
||||
task4,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
task1,
|
||||
task2,
|
||||
task3,
|
||||
task5,
|
||||
},
|
||||
wantDesc: []*Task{
|
||||
task5,
|
||||
task3,
|
||||
task2,
|
||||
task1,
|
||||
task4,
|
||||
task6,
|
||||
task7,
|
||||
task8,
|
||||
task9,
|
||||
task10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestTaskSort(t *testing.T) {
|
||||
|
||||
assertTestSliceMatch := func(t *testing.T, got, want []*Task) {
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Error("Slices do not match in order")
|
||||
t.Error("Got\t| Want")
|
||||
for in, task := range got {
|
||||
fail := ""
|
||||
if task.ID != want[in].ID {
|
||||
fail = "wrong"
|
||||
}
|
||||
t.Errorf("\t%d\t| %d \t%s", task.ID, want[in].ID, fail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, testCase := range taskSortTestCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Run("asc default", func(t *testing.T) {
|
||||
by := []*sortParam{
|
||||
{
|
||||
sortBy: sortProperty(testCase.sortProperty),
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(testCase.wantAsc).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, by)
|
||||
|
||||
assertTestSliceMatch(t, got, testCase.wantAsc)
|
||||
})
|
||||
t.Run("asc", func(t *testing.T) {
|
||||
by := []*sortParam{
|
||||
{
|
||||
sortBy: sortProperty(testCase.sortProperty),
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(testCase.wantAsc).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, by)
|
||||
|
||||
assertTestSliceMatch(t, got, testCase.wantAsc)
|
||||
})
|
||||
t.Run("desc", func(t *testing.T) {
|
||||
by := []*sortParam{
|
||||
{
|
||||
sortBy: sortProperty(testCase.sortProperty),
|
||||
orderBy: orderDescending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(testCase.wantDesc).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, by)
|
||||
|
||||
assertTestSliceMatch(t, got, testCase.wantDesc)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Other cases
|
||||
t.Run("Order by Done Ascending and ID Descending", func(t *testing.T) {
|
||||
want := []*Task{
|
||||
// Not done
|
||||
task10,
|
||||
task8,
|
||||
task7,
|
||||
task6,
|
||||
task5,
|
||||
task4,
|
||||
task3,
|
||||
|
||||
// Done
|
||||
task9,
|
||||
task2,
|
||||
task1,
|
||||
}
|
||||
sortParams := []*sortParam{
|
||||
{
|
||||
sortBy: sortProperty(taskPropertyDone),
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
{
|
||||
sortBy: sortProperty(taskPropertyID),
|
||||
orderBy: orderDescending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(want).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, sortParams)
|
||||
|
||||
assertTestSliceMatch(t, got, want)
|
||||
})
|
||||
t.Run("Order by Done Ascending and Text Descending", func(t *testing.T) {
|
||||
want := []*Task{
|
||||
// Not done
|
||||
task10,
|
||||
task7,
|
||||
task5,
|
||||
task6,
|
||||
task4,
|
||||
task3,
|
||||
task8,
|
||||
// Done
|
||||
task2,
|
||||
task1,
|
||||
task9,
|
||||
}
|
||||
sortParams := []*sortParam{
|
||||
{
|
||||
sortBy: sortProperty(taskPropertyDone),
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
{
|
||||
sortBy: sortProperty(taskPropertyText),
|
||||
orderBy: orderDescending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(want).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, sortParams)
|
||||
|
||||
assertTestSliceMatch(t, got, want)
|
||||
})
|
||||
t.Run("Order by Done Descending and Text Ascending", func(t *testing.T) {
|
||||
want := []*Task{
|
||||
// Done
|
||||
task9,
|
||||
task1,
|
||||
task2,
|
||||
// Not done
|
||||
task8,
|
||||
task3,
|
||||
task4,
|
||||
task5,
|
||||
task6,
|
||||
task7,
|
||||
task10,
|
||||
}
|
||||
sortParams := []*sortParam{
|
||||
{
|
||||
sortBy: sortProperty(taskPropertyDone),
|
||||
orderBy: orderDescending,
|
||||
},
|
||||
{
|
||||
sortBy: sortProperty(taskPropertyText),
|
||||
orderBy: orderAscending,
|
||||
},
|
||||
}
|
||||
|
||||
got := deepcopy.Copy(want).([]*Task)
|
||||
|
||||
// Destroy wanted order to obtain some slice we can sort
|
||||
rand.Shuffle(len(got), func(i, j int) {
|
||||
got[i], got[j] = got[j], got[i]
|
||||
})
|
||||
|
||||
sortTasks(got, sortParams)
|
||||
|
||||
assertTestSliceMatch(t, got, want)
|
||||
|
||||
})
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
// We use individual variables for the tasks here to be able to rearrange or remove ones more easily
|
||||
task1 := &Task{
|
||||
ID: 1,
|
||||
Text: "task #1",
|
||||
Title: "task #1",
|
||||
Description: "Lorem Ipsum",
|
||||
Identifier: "test1-1",
|
||||
Index: 1,
|
||||
|
@ -71,10 +71,11 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
RelationKindSubtask: {
|
||||
{
|
||||
ID: 29,
|
||||
Text: "task #29 with parent task (1)",
|
||||
Title: "task #29 with parent task (1)",
|
||||
Index: 14,
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
|
@ -108,7 +109,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task2 := &Task{
|
||||
ID: 2,
|
||||
Text: "task #2 done",
|
||||
Title: "task #2 done",
|
||||
Identifier: "test1-2",
|
||||
Index: 2,
|
||||
Done: true,
|
||||
|
@ -132,7 +133,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task3 := &Task{
|
||||
ID: 3,
|
||||
Text: "task #3 high prio",
|
||||
Title: "task #3 high prio",
|
||||
Identifier: "test1-3",
|
||||
Index: 3,
|
||||
CreatedByID: 1,
|
||||
|
@ -146,7 +147,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task4 := &Task{
|
||||
ID: 4,
|
||||
Text: "task #4 low prio",
|
||||
Title: "task #4 low prio",
|
||||
Identifier: "test1-4",
|
||||
Index: 4,
|
||||
CreatedByID: 1,
|
||||
|
@ -160,7 +161,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task5 := &Task{
|
||||
ID: 5,
|
||||
Text: "task #5 higher due date",
|
||||
Title: "task #5 higher due date",
|
||||
Identifier: "test1-5",
|
||||
Index: 5,
|
||||
CreatedByID: 1,
|
||||
|
@ -174,7 +175,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task6 := &Task{
|
||||
ID: 6,
|
||||
Text: "task #6 lower due date",
|
||||
Title: "task #6 lower due date",
|
||||
Identifier: "test1-6",
|
||||
Index: 6,
|
||||
CreatedByID: 1,
|
||||
|
@ -188,7 +189,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task7 := &Task{
|
||||
ID: 7,
|
||||
Text: "task #7 with start date",
|
||||
Title: "task #7 with start date",
|
||||
Identifier: "test1-7",
|
||||
Index: 7,
|
||||
CreatedByID: 1,
|
||||
|
@ -202,7 +203,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task8 := &Task{
|
||||
ID: 8,
|
||||
Text: "task #8 with end date",
|
||||
Title: "task #8 with end date",
|
||||
Identifier: "test1-8",
|
||||
Index: 8,
|
||||
CreatedByID: 1,
|
||||
|
@ -216,13 +217,14 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task9 := &Task{
|
||||
ID: 9,
|
||||
Text: "task #9 with start and end date",
|
||||
Title: "task #9 with start and end date",
|
||||
Identifier: "test1-9",
|
||||
Index: 9,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDate: 1544600000,
|
||||
|
@ -230,200 +232,216 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
}
|
||||
task10 := &Task{
|
||||
ID: 10,
|
||||
Text: "task #10 basic",
|
||||
Title: "task #10 basic",
|
||||
Identifier: "test1-10",
|
||||
Index: 10,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task11 := &Task{
|
||||
ID: 11,
|
||||
Text: "task #11 basic",
|
||||
Title: "task #11 basic",
|
||||
Identifier: "test1-11",
|
||||
Index: 11,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task12 := &Task{
|
||||
ID: 12,
|
||||
Text: "task #12 basic",
|
||||
Title: "task #12 basic",
|
||||
Identifier: "test1-12",
|
||||
Index: 12,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task15 := &Task{
|
||||
ID: 15,
|
||||
Text: "task #15",
|
||||
Title: "task #15",
|
||||
Identifier: "test6-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 6,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 6,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task16 := &Task{
|
||||
ID: 16,
|
||||
Text: "task #16",
|
||||
Title: "task #16",
|
||||
Identifier: "test7-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 7,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 7,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task17 := &Task{
|
||||
ID: 17,
|
||||
Text: "task #17",
|
||||
Title: "task #17",
|
||||
Identifier: "test8-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 8,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 8,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task18 := &Task{
|
||||
ID: 18,
|
||||
Text: "task #18",
|
||||
Title: "task #18",
|
||||
Identifier: "test9-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 9,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 9,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task19 := &Task{
|
||||
ID: 19,
|
||||
Text: "task #19",
|
||||
Title: "task #19",
|
||||
Identifier: "test10-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 10,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 10,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task20 := &Task{
|
||||
ID: 20,
|
||||
Text: "task #20",
|
||||
Title: "task #20",
|
||||
Identifier: "test11-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 11,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 11,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task21 := &Task{
|
||||
ID: 21,
|
||||
Text: "task #21",
|
||||
Title: "task #21",
|
||||
Identifier: "test12-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 12,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 12,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task22 := &Task{
|
||||
ID: 22,
|
||||
Text: "task #22",
|
||||
Title: "task #22",
|
||||
Identifier: "test13-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 13,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 13,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task23 := &Task{
|
||||
ID: 23,
|
||||
Text: "task #23",
|
||||
Title: "task #23",
|
||||
Identifier: "test14-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 14,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 14,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task24 := &Task{
|
||||
ID: 24,
|
||||
Text: "task #24",
|
||||
Title: "task #24",
|
||||
Identifier: "test15-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 15,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 15,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task25 := &Task{
|
||||
ID: 25,
|
||||
Text: "task #25",
|
||||
Title: "task #25",
|
||||
Identifier: "test16-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 16,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 16,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task26 := &Task{
|
||||
ID: 26,
|
||||
Text: "task #26",
|
||||
Title: "task #26",
|
||||
Identifier: "test17-1",
|
||||
Index: 1,
|
||||
CreatedByID: 6,
|
||||
CreatedBy: user6,
|
||||
ListID: 17,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 17,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task27 := &Task{
|
||||
ID: 27,
|
||||
Text: "task #27 with reminders",
|
||||
Title: "task #27 with reminders",
|
||||
Identifier: "test1-12",
|
||||
Index: 12,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
Reminders: []timeutil.TimeStamp{1543626724, 1543626824},
|
||||
ListID: 1,
|
||||
BucketID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task28 := &Task{
|
||||
ID: 28,
|
||||
Text: "task #28 with repeat after",
|
||||
Title: "task #28 with repeat after",
|
||||
Identifier: "test1-13",
|
||||
Index: 13,
|
||||
CreatedByID: 1,
|
||||
|
@ -431,12 +449,13 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
RepeatAfter: 3600,
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task29 := &Task{
|
||||
ID: 29,
|
||||
Text: "task #29 with parent task (1)",
|
||||
Title: "task #29 with parent task (1)",
|
||||
Identifier: "test1-14",
|
||||
Index: 14,
|
||||
CreatedByID: 1,
|
||||
|
@ -446,7 +465,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
RelationKindParenttask: {
|
||||
{
|
||||
ID: 1,
|
||||
Text: "task #1",
|
||||
Title: "task #1",
|
||||
Description: "Lorem Ipsum",
|
||||
Index: 1,
|
||||
CreatedByID: 1,
|
||||
|
@ -457,12 +476,13 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task30 := &Task{
|
||||
ID: 30,
|
||||
Text: "task #30 with assignees",
|
||||
Title: "task #30 with assignees",
|
||||
Identifier: "test1-15",
|
||||
Index: 15,
|
||||
CreatedByID: 1,
|
||||
|
@ -473,12 +493,13 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
user2,
|
||||
},
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task31 := &Task{
|
||||
ID: 31,
|
||||
Text: "task #31 with color",
|
||||
Title: "task #31 with color",
|
||||
Identifier: "test1-16",
|
||||
Index: 16,
|
||||
HexColor: "f0f0f0",
|
||||
|
@ -486,24 +507,26 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
CreatedBy: user1,
|
||||
ListID: 1,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task32 := &Task{
|
||||
ID: 32,
|
||||
Text: "task #32",
|
||||
Title: "task #32",
|
||||
Identifier: "test3-1",
|
||||
Index: 1,
|
||||
CreatedByID: 1,
|
||||
CreatedBy: user1,
|
||||
ListID: 3,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 21,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
task33 := &Task{
|
||||
ID: 33,
|
||||
Text: "task #33 with percent done",
|
||||
Title: "task #33 with percent done",
|
||||
Identifier: "test1-17",
|
||||
Index: 17,
|
||||
CreatedByID: 1,
|
||||
|
@ -511,6 +534,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
ListID: 1,
|
||||
PercentDone: 0.5,
|
||||
RelatedTasks: map[RelationKind][]*Task{},
|
||||
BucketID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/timeutil"
|
||||
|
@ -24,10 +25,11 @@ import (
|
|||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/imdario/mergo"
|
||||
"sort"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// Task represents an task in a todolist
|
||||
|
@ -35,7 +37,7 @@ type Task struct {
|
|||
// The unique, numeric id of this task.
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
// The task text. This is what you'll see in the list.
|
||||
Text string `xorm:"varchar(250) not null" json:"text" valid:"runelength(3|250)" minLength:"3" maxLength:"250"`
|
||||
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(3|250)" minLength:"3" maxLength:"250"`
|
||||
// The task description.
|
||||
Description string `xorm:"longtext null" json:"description"`
|
||||
// Whether a task is done or not.
|
||||
|
@ -88,6 +90,14 @@ type Task struct {
|
|||
// BucketID is the ID of the kanban bucket this task belongs to.
|
||||
BucketID int64 `xorm:"int(11) null" json:"bucket_id"`
|
||||
|
||||
// The position of the task - any task list can be sorted as usual by this parameter.
|
||||
// When accessing tasks via kanban buckets, this is primarily used to sort them based on a range
|
||||
// We're using a float64 here to make it possible to put any task within any two other tasks (by changing the number).
|
||||
// You would calculate the new position between two tasks with something like task3.position = (task2.position - task1.position) / 2.
|
||||
// A 64-Bit float leaves plenty of room to initially give tasks a position with 2^16 difference to the previous task
|
||||
// which also leaves a lot of room for rearranging and sorting later.
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
|
||||
// The user who initially created the task.
|
||||
CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"`
|
||||
|
||||
|
@ -113,12 +123,20 @@ func (TaskReminder) TableName() string {
|
|||
return "task_reminders"
|
||||
}
|
||||
|
||||
type taskFilterConcatinator string
|
||||
|
||||
const (
|
||||
filterConcatAnd = "and"
|
||||
filterConcatOr = "or"
|
||||
)
|
||||
|
||||
type taskOptions struct {
|
||||
search string
|
||||
page int
|
||||
perPage int
|
||||
sortby []*sortParam
|
||||
filters []*taskFilter
|
||||
search string
|
||||
page int
|
||||
perPage int
|
||||
sortby []*sortParam
|
||||
filters []*taskFilter
|
||||
filterConcat taskFilterConcatinator
|
||||
}
|
||||
|
||||
// ReadAll is a dummy function to still have that endpoint documented
|
||||
|
@ -135,6 +153,7 @@ type taskOptions struct {
|
|||
// @Param filter_by query string false "The name of the field to filter by. Accepts an array for multiple filters which will be chanied together, all supplied filter must match."
|
||||
// @Param filter_value query string false "The value to filter for."
|
||||
// @Param filter_comparator query string false "The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less` and `less_equals`. Defaults to `equals`"
|
||||
// @Param filter_concat query string false "The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`."
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {array} models.Task "The tasks"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
|
@ -143,7 +162,17 @@ func (t *Task) ReadAll(a web.Auth, search string, page int, perPage int) (result
|
|||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*Task, resultCount int, totalItems int64, err error) {
|
||||
func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||
|
||||
// If the user does not have any lists, don't try to get any tasks
|
||||
if len(lists) == 0 {
|
||||
return nil, 0, 0, nil
|
||||
}
|
||||
|
||||
// Set the default concatinator of filter variables to or if none was provided
|
||||
if opts.filterConcat == "" {
|
||||
opts.filterConcat = filterConcatOr
|
||||
}
|
||||
|
||||
// Get all list IDs and get the tasks
|
||||
var listIDs []int64
|
||||
|
@ -151,6 +180,15 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
|||
listIDs = append(listIDs, l.ID)
|
||||
}
|
||||
|
||||
// Add the id parameter as the last parameter to sorty by default, but only if it is not already passed as the last parameter.
|
||||
if len(opts.sortby) == 0 ||
|
||||
len(opts.sortby) > 0 && opts.sortby[len(opts.sortby)-1].sortBy != taskPropertyID {
|
||||
opts.sortby = append(opts.sortby, &sortParam{
|
||||
sortBy: taskPropertyID,
|
||||
orderBy: orderAscending,
|
||||
})
|
||||
}
|
||||
|
||||
// Since xorm does not use placeholders for order by, it is possible to expose this with sql injection if we're directly
|
||||
// passing user input to the db.
|
||||
// As a workaround to prevent this, we check for valid column names here prior to passing it to the db.
|
||||
|
@ -160,13 +198,26 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
|||
if err := param.validate(); err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
orderby += param.sortBy.String() + " " + param.orderBy.String()
|
||||
orderby += param.sortBy + " " + param.orderBy.String()
|
||||
|
||||
// Postgres sorts by default entries with null values after ones with values.
|
||||
// To make that consistent with the sort order we have and other dbms, we're adding a separate clause here.
|
||||
if x.Dialect().URI().DBType == schemas.POSTGRES {
|
||||
if param.orderBy == orderAscending {
|
||||
orderby += " NULLS FIRST"
|
||||
}
|
||||
if param.orderBy == orderDescending {
|
||||
orderby += " NULLS LAST"
|
||||
}
|
||||
}
|
||||
|
||||
if (i + 1) < len(opts.sortby) {
|
||||
orderby += ", "
|
||||
}
|
||||
}
|
||||
|
||||
var filters = make([]builder.Cond, 0, len(opts.filters))
|
||||
// To still find tasks with nil values, we exclude 0s when comparing with >/< values.
|
||||
for _, f := range opts.filters {
|
||||
switch f.comparator {
|
||||
case taskFilterComparatorEquals:
|
||||
|
@ -174,32 +225,50 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
|||
case taskFilterComparatorNotEquals:
|
||||
filters = append(filters, &builder.Neq{f.field: f.value})
|
||||
case taskFilterComparatorGreater:
|
||||
filters = append(filters, &builder.Gt{f.field: f.value})
|
||||
filters = append(filters, builder.Or(&builder.Gt{f.field: f.value}, &builder.Eq{f.field: 0}))
|
||||
case taskFilterComparatorGreateEquals:
|
||||
filters = append(filters, &builder.Gte{f.field: f.value})
|
||||
filters = append(filters, builder.Or(&builder.Gte{f.field: f.value}, &builder.Eq{f.field: 0}))
|
||||
case taskFilterComparatorLess:
|
||||
filters = append(filters, &builder.Lt{f.field: f.value})
|
||||
filters = append(filters, builder.Or(&builder.Lt{f.field: f.value}, &builder.Eq{f.field: 0}))
|
||||
case taskFilterComparatorLessEquals:
|
||||
filters = append(filters, &builder.Lte{f.field: f.value})
|
||||
filters = append(filters, builder.Or(&builder.Lte{f.field: f.value}, &builder.Eq{f.field: 0}))
|
||||
}
|
||||
}
|
||||
|
||||
taskMap = make(map[int64]*Task)
|
||||
|
||||
// Then return all tasks for that lists
|
||||
query := x.
|
||||
query := x.NewSession().
|
||||
OrderBy(orderby)
|
||||
queryCount := x.NewSession()
|
||||
|
||||
if len(opts.search) > 0 {
|
||||
query = query.Where("text LIKE ?", "%"+opts.search+"%")
|
||||
// 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
|
||||
// Seems okay to use that now, we may need to find a better solution overall in the future.
|
||||
if config.DatabaseType.GetString() == "postgres" {
|
||||
query = query.Where("title ILIKE ?", "%"+opts.search+"%")
|
||||
queryCount = queryCount.Where("title ILIKE ?", "%"+opts.search+"%")
|
||||
} else {
|
||||
query = query.Where("title LIKE ?", "%"+opts.search+"%")
|
||||
queryCount = queryCount.Where("title LIKE ?", "%"+opts.search+"%")
|
||||
}
|
||||
}
|
||||
|
||||
if len(listIDs) > 0 {
|
||||
query = query.In("list_id", listIDs)
|
||||
queryCount = queryCount.In("list_id", listIDs)
|
||||
}
|
||||
|
||||
if len(filters) > 0 {
|
||||
query = query.Where(builder.Or(filters...))
|
||||
if opts.filterConcat == filterConcatOr {
|
||||
query = query.Where(builder.Or(filters...))
|
||||
queryCount = queryCount.Where(builder.Or(filters...))
|
||||
}
|
||||
if opts.filterConcat == filterConcatAnd {
|
||||
query = query.Where(builder.And(filters...))
|
||||
queryCount = queryCount.Where(builder.And(filters...))
|
||||
}
|
||||
}
|
||||
|
||||
limit, start := getLimitFromPageIndex(opts.page, opts.perPage)
|
||||
|
@ -208,34 +277,37 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (taskMap map[int64]*T
|
|||
query = query.Limit(limit, start)
|
||||
}
|
||||
|
||||
err = query.Find(&taskMap)
|
||||
tasks = []*Task{}
|
||||
err = query.Find(&tasks)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
totalItems, err = query.
|
||||
totalItems, err = queryCount.
|
||||
Count(&Task{})
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
return taskMap, len(taskMap), totalItems, nil
|
||||
return tasks, len(tasks), totalItems, nil
|
||||
}
|
||||
|
||||
func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
|
||||
|
||||
taskMap, resultCount, totalItems, err := getRawTasksForLists(lists, opts)
|
||||
tasks, resultCount, totalItems, err = getRawTasksForLists(lists, opts)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
tasks, err = addMoreInfoToTasks(taskMap)
|
||||
taskMap := make(map[int64]*Task, len(tasks))
|
||||
for _, t := range tasks {
|
||||
taskMap[t.ID] = t
|
||||
}
|
||||
|
||||
err = addMoreInfoToTasks(taskMap)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
// Because the list is fully unsorted (since we're dealing with maps)
|
||||
// we have to manually sort the tasks again here.
|
||||
sortTasks(tasks, opts.sortby)
|
||||
|
||||
return tasks, resultCount, totalItems, err
|
||||
}
|
||||
|
@ -271,25 +343,28 @@ func (bt *BulkTask) GetTasksByIDs() (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
taskMap := make(map[int64]*Task, len(bt.Tasks))
|
||||
err = x.In("id", bt.IDs).Find(&taskMap)
|
||||
err = x.In("id", bt.IDs).Find(&bt.Tasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
bt.Tasks, err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
// GetTasksByUIDs gets all tasks from a bunch of uids
|
||||
func GetTasksByUIDs(uids []string) (tasks []*Task, err error) {
|
||||
taskMap := make(map[int64]*Task)
|
||||
err = x.In("uid", uids).Find(&taskMap)
|
||||
tasks = []*Task{}
|
||||
err = x.In("uid", uids).Find(&tasks)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tasks, err = addMoreInfoToTasks(taskMap)
|
||||
taskMap := make(map[int64]*Task, len(tasks))
|
||||
for _, t := range tasks {
|
||||
taskMap[t.ID] = t
|
||||
}
|
||||
|
||||
err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -301,7 +376,7 @@ func getRemindersForTasks(taskIDs []int64) (reminders []*TaskReminder, err error
|
|||
|
||||
// This function takes a map with pointers and returns a slice with pointers to tasks
|
||||
// It adds more stuff like assignees/labels/etc to a bunch of tasks
|
||||
func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
|
||||
func addMoreInfoToTasks(taskMap map[int64]*Task) (err error) {
|
||||
|
||||
// No need to iterate over users and stuff if the list doesn't has tasks
|
||||
if len(taskMap) == 0 {
|
||||
|
@ -351,7 +426,7 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
|
|||
In("task_id", taskIDs).
|
||||
Find(&attachments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
fileIDs := []int64{}
|
||||
|
@ -446,19 +521,6 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
|
|||
taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], fullRelatedTasks[rt.OtherTaskID])
|
||||
}
|
||||
|
||||
// make a complete slice from the map
|
||||
tasks = []*Task{}
|
||||
for _, t := range taskMap {
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
|
||||
// Sort the output. In Go, contents on a map are put on that map in no particular order.
|
||||
// Because of this, tasks are not sorted anymore in the output, this leads to confiusion.
|
||||
// To avoid all this, we need to sort the slice afterwards
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].ID < tasks[j].ID
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -497,7 +559,7 @@ func (t *Task) Create(a web.Auth) (err error) {
|
|||
t.ID = 0
|
||||
|
||||
// Check if we have at least a text
|
||||
if t.Text == "" {
|
||||
if t.Title == "" {
|
||||
return ErrTaskCannotBeEmpty{}
|
||||
}
|
||||
|
||||
|
@ -507,9 +569,16 @@ func (t *Task) Create(a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
u, err := user.GetUserByID(a.GetID())
|
||||
if err != nil {
|
||||
return err
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
// A negative user id indicates user share links
|
||||
t.CreatedByID = a.GetID() * -1
|
||||
} else {
|
||||
u, err := user.GetUserByID(a.GetID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.CreatedByID = u.ID
|
||||
t.CreatedBy = u
|
||||
}
|
||||
|
||||
// Generate a uuid if we don't already have one
|
||||
|
@ -523,6 +592,15 @@ func (t *Task) Create(a web.Auth) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Get the default bucket and move the task there
|
||||
if t.BucketID == 0 {
|
||||
defaultBucket, err := getDefaultBucket(t.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.BucketID = defaultBucket.ID
|
||||
}
|
||||
|
||||
// Get the index for this task
|
||||
latestTask := &Task{}
|
||||
_, err = x.Where("list_id = ?", t.ListID).OrderBy("id desc").Get(latestTask)
|
||||
|
@ -531,8 +609,10 @@ func (t *Task) Create(a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
t.Index = latestTask.Index + 1
|
||||
t.CreatedByID = u.ID
|
||||
t.CreatedBy = u
|
||||
// If no position was supplied, set a default one
|
||||
if t.Position == 0 {
|
||||
t.Position = float64(latestTask.ID+1) * math.Pow(2, 16)
|
||||
}
|
||||
if _, err = x.Insert(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -604,6 +684,15 @@ func (t *Task) Update() (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// If the task is being moved between lists, make sure to move the bucket as well
|
||||
if t.ListID != 0 && ot.ListID != t.ListID {
|
||||
b, err := getDefaultBucket(t.ListID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.BucketID = b.ID
|
||||
}
|
||||
|
||||
// Update the labels
|
||||
//
|
||||
// Maybe FIXME:
|
||||
|
@ -668,14 +757,13 @@ func (t *Task) Update() (err error) {
|
|||
if t.PercentDone == 0 {
|
||||
ot.PercentDone = 0
|
||||
}
|
||||
// Bucket ID
|
||||
// Yes it is possible to move a task back into the empty bucket
|
||||
if t.BucketID == 0 {
|
||||
ot.BucketID = 0
|
||||
// Position
|
||||
if t.Position == 0 {
|
||||
ot.Position = 0
|
||||
}
|
||||
|
||||
_, err = x.ID(t.ID).
|
||||
Cols("text",
|
||||
Cols("title",
|
||||
"description",
|
||||
"done",
|
||||
"due_date_unix",
|
||||
|
@ -688,6 +776,7 @@ func (t *Task) Update() (err error) {
|
|||
"percent_done",
|
||||
"list_id",
|
||||
"bucket_id",
|
||||
"position",
|
||||
).
|
||||
Update(ot)
|
||||
*t = ot
|
||||
|
@ -877,16 +966,16 @@ func (t *Task) ReadOne() (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
tasks, err := addMoreInfoToTasks(taskMap)
|
||||
err = addMoreInfoToTasks(taskMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
if len(taskMap) == 0 {
|
||||
return ErrTaskDoesNotExist{t.ID}
|
||||
}
|
||||
|
||||
*t = *tasks[0]
|
||||
*t = *taskMap[t.ID]
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ func TestTask_Create(t *testing.T) {
|
|||
t.Run("normal", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
task := &Task{
|
||||
Text: "Lorem",
|
||||
Title: "Lorem",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
}
|
||||
|
@ -47,12 +47,14 @@ func TestTask_Create(t *testing.T) {
|
|||
// Assert getting a new index
|
||||
assert.NotEmpty(t, task.Index)
|
||||
assert.Equal(t, int64(18), task.Index)
|
||||
// Assert moving it into the default bucket
|
||||
assert.Equal(t, int64(1), task.BucketID)
|
||||
|
||||
})
|
||||
t.Run("empty text", func(t *testing.T) {
|
||||
t.Run("empty title", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
task := &Task{
|
||||
Text: "",
|
||||
Title: "",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
}
|
||||
|
@ -63,7 +65,7 @@ func TestTask_Create(t *testing.T) {
|
|||
t.Run("nonexistant list", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
task := &Task{
|
||||
Text: "Test",
|
||||
Title: "Test",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 9999999,
|
||||
}
|
||||
|
@ -75,7 +77,7 @@ func TestTask_Create(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
nUser := &user.User{ID: 99999999}
|
||||
task := &Task{
|
||||
Text: "Test",
|
||||
Title: "Test",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
}
|
||||
|
@ -90,7 +92,7 @@ func TestTask_Update(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
task := &Task{
|
||||
ID: 1,
|
||||
Text: "test10000",
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
}
|
||||
|
@ -101,7 +103,7 @@ func TestTask_Update(t *testing.T) {
|
|||
db.LoadAndAssertFixtures(t)
|
||||
task := &Task{
|
||||
ID: 9999999,
|
||||
Text: "test10000",
|
||||
Title: "test10000",
|
||||
Description: "Lorem Ipsum Dolor",
|
||||
ListID: 1,
|
||||
}
|
||||
|
@ -213,7 +215,7 @@ func TestTask_ReadOne(t *testing.T) {
|
|||
task := &Task{ID: 1}
|
||||
err := task.ReadOne()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "task #1", task.Text)
|
||||
assert.Equal(t, "task #1", task.Title)
|
||||
})
|
||||
t.Run("nonexisting", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import "code.vikunja.io/api/pkg/files"
|
||||
|
||||
// Unsplash requires us to do pingbacks to their site and also name the image author.
|
||||
// To do this properly, we need to save these information somewhere.
|
||||
|
||||
// UnsplashPhoto is an unsplash photo in the db
|
||||
type UnsplashPhoto struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id,omitempty"`
|
||||
FileID int64 `xorm:"not null" json:"-"`
|
||||
UnsplashID string `xorm:"varchar(50)" json:"unsplash_id"`
|
||||
Author string `xorm:"text" json:"author"`
|
||||
AuthorName string `xorm:"text" json:"author_name"`
|
||||
}
|
||||
|
||||
// TableName contains the table name for an unsplash photo
|
||||
func (u *UnsplashPhoto) TableName() string {
|
||||
return "unsplash_photos"
|
||||
}
|
||||
|
||||
// Save persists an unsplash photo to the db
|
||||
func (u *UnsplashPhoto) Save() error {
|
||||
_, err := x.Insert(u)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetUnsplashPhotoByFileID returns an unsplash photo by its saved file id
|
||||
func GetUnsplashPhotoByFileID(fileID int64) (u *UnsplashPhoto, err error) {
|
||||
u = &UnsplashPhoto{}
|
||||
exists, err := x.Where("file_id = ?", fileID).Get(u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
return nil, files.ErrFileIsNotUnsplashFile{FileID: fileID}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveUnsplashPhoto removes an unsplash photo from the db
|
||||
func RemoveUnsplashPhoto(fileID int64) (err error) {
|
||||
// This is intentionally "fire and forget" which is why we don't check if we have an
|
||||
// unsplash entry for that file at all. If there is one, it will be deleted.
|
||||
// We do this to keep the function simple.
|
||||
_, err = x.Where("file_id = ?", fileID).Delete(&UnsplashPhoto{})
|
||||
return
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package background
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/web"
|
||||
)
|
||||
|
||||
// Image represents an image which can be used as a list background
|
||||
type Image struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Thumb string `json:"thumb,omitempty"`
|
||||
// This can be used to supply extra information from an image provider to clients
|
||||
Info interface{} `json:"info,omitempty"`
|
||||
}
|
||||
|
||||
// Provider represents something that is able to get a list of images and set one of them as background
|
||||
type Provider interface {
|
||||
// Search is used to either return a pre-defined list of Image or let the user search for an image
|
||||
Search(search string, page int64) (result []*Image, err error)
|
||||
// Set sets an image which was most likely previously obtained by Search as list background
|
||||
Set(image *Image, list *models.List, auth web.Auth) (err error)
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/background"
|
||||
"code.vikunja.io/api/pkg/modules/background/unsplash"
|
||||
v1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// BackgroundProvider represents a thing which holds a background provider
|
||||
// Lets us get a new fresh provider every time we need one.
|
||||
type BackgroundProvider struct {
|
||||
Provider func() background.Provider
|
||||
}
|
||||
|
||||
// SearchBackgrounds is the web handler to search for backgrounds
|
||||
func (bp *BackgroundProvider) SearchBackgrounds(c echo.Context) error {
|
||||
p := bp.Provider()
|
||||
|
||||
err := c.Bind(p)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error())
|
||||
}
|
||||
|
||||
search := c.QueryParam("s")
|
||||
var page int64 = 1
|
||||
pg := c.QueryParam("p")
|
||||
if pg != "" {
|
||||
page, err = strconv.ParseInt(pg, 10, 64)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid page number: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
result, err := p.Search(search, page)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "An error occurred: "+err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// SetBackground sets an Image as list background
|
||||
func (bp *BackgroundProvider) SetBackground(c echo.Context) error {
|
||||
auth, err := v1.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error())
|
||||
}
|
||||
|
||||
p := bp.Provider()
|
||||
|
||||
listID, err := strconv.ParseInt(c.Param("list"), 10, 64)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid list ID: "+err.Error())
|
||||
}
|
||||
|
||||
// Check if the user has the right to change the list background
|
||||
list := &models.List{ID: listID}
|
||||
can, err := list.CanUpdate(auth)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
if !can {
|
||||
log.Infof("Tried to update list background of list %d while not having the rights for it (User: %v)", listID, auth)
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
}
|
||||
// Load the list
|
||||
if err := list.GetSimpleByID(); err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
image := &background.Image{}
|
||||
err = c.Bind(image)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No or invalid model provided: "+err.Error())
|
||||
}
|
||||
|
||||
err = p.Set(image, list, auth)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
return c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// GetListBackground serves a previously set background from a list
|
||||
// It has no knowledge of the provider that was responsible for setting the background.
|
||||
// @Summary Get the list background
|
||||
// @Description Get the list background of a specific list. **Returns json on error.**
|
||||
// @tags list
|
||||
// @Produce octet-stream
|
||||
// @Param id path int true "List ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {} string "The list background file."
|
||||
// @Failure 403 {object} models.Message "No access to this list."
|
||||
// @Failure 404 {object} models.Message "The list does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/background [get]
|
||||
func GetListBackground(c echo.Context) error {
|
||||
|
||||
auth, err := v1.GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error())
|
||||
}
|
||||
|
||||
listID, err := strconv.ParseInt(c.Param("list"), 10, 64)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid list ID: "+err.Error())
|
||||
}
|
||||
|
||||
// Check if a background for this list exists + Rights
|
||||
list := &models.List{ID: listID}
|
||||
can, err := list.CanRead(auth)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
if !can {
|
||||
log.Infof("Tried to get list background of list %d while not having the rights for it (User: %v)", listID, auth)
|
||||
return echo.NewHTTPError(http.StatusForbidden)
|
||||
}
|
||||
if list.BackgroundFileID == 0 {
|
||||
return echo.NotFoundHandler(c)
|
||||
}
|
||||
|
||||
// Get the file
|
||||
bgFile := &files.File{
|
||||
ID: list.BackgroundFileID,
|
||||
}
|
||||
if err := bgFile.LoadFileByID(); err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
// Unsplash requires pingbacks as per their api usage guidelines.
|
||||
// To do this in a privacy-preserving manner, we do the ping from inside of Vikunja to not expose any user details.
|
||||
// FIXME: This should use an event once we have events
|
||||
unsplash.Pingback(bgFile)
|
||||
|
||||
// Serve the file
|
||||
return c.Stream(http.StatusOK, "image/jpg", bgFile.File)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package unsplash
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ProxyUnsplashImage proxies a thumbnail from unsplash for privacy reasons.
|
||||
// @Summary Get an unsplash image
|
||||
// @Description Get an unsplash image. **Returns json on error.**
|
||||
// @tags list
|
||||
// @Produce octet-stream
|
||||
// @Param thumb path int true "Unsplash Image ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {} string "The image"
|
||||
// @Failure 404 {object} models.Message "The image does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /backgrounds/unsplash/image/{image} [get]
|
||||
func ProxyUnsplashImage(c echo.Context) error {
|
||||
imageID := c.Param("image")
|
||||
resp, err := http.Get("https://images.unsplash.com/photo-" + imageID + "?ixlib=rb-1.2.1&fm=jpg&ixid=eyJhcHBfaWQiOjcyODAwfQ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode > 399 {
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
return c.Stream(http.StatusOK, "image/jpg", resp.Body)
|
||||
}
|
||||
|
||||
// ProxyUnsplashThumb proxies a thumbnail from unsplash for privacy reasons.
|
||||
// @Summary Get an unsplash thumbnail image
|
||||
// @Description Get an unsplash thumbnail image. The thumbnail is cropped to a max width of 200px. **Returns json on error.**
|
||||
// @tags list
|
||||
// @Produce octet-stream
|
||||
// @Param thumb path int true "Unsplash Image ID"
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {} string "The thumbnail"
|
||||
// @Failure 404 {object} models.Message "The image does not exist."
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /backgrounds/unsplash/image/{image}/thumb [get]
|
||||
func ProxyUnsplashThumb(c echo.Context) error {
|
||||
imageID := c.Param("image")
|
||||
resp, err := http.Get("https://images.unsplash.com/photo-" + imageID + "?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjcyODAwfQ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode > 399 {
|
||||
return echo.ErrNotFound
|
||||
}
|
||||
return c.Stream(http.StatusOK, "image/jpg", resp.Body)
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package unsplash
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/background"
|
||||
"code.vikunja.io/web"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Provider represents an unsplash image provider
|
||||
type Provider struct {
|
||||
}
|
||||
|
||||
// SearchResult is a search result from unsplash's api
|
||||
type SearchResult struct {
|
||||
Total int `json:"total"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
Results []*Photo `json:"results"`
|
||||
}
|
||||
|
||||
// Photo represents an unpslash photo as returned by their api
|
||||
type Photo struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Color string `json:"color"`
|
||||
Description string `json:"description"`
|
||||
User struct {
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
} `json:"user"`
|
||||
Urls struct {
|
||||
Raw string `json:"raw"`
|
||||
Full string `json:"full"`
|
||||
Regular string `json:"regular"`
|
||||
Small string `json:"small"`
|
||||
Thumb string `json:"thumb"`
|
||||
} `json:"urls"`
|
||||
Links struct {
|
||||
Self string `json:"self"`
|
||||
HTML string `json:"html"`
|
||||
Download string `json:"download"`
|
||||
} `json:"links"`
|
||||
}
|
||||
|
||||
// Very simple caching method - pretty much only used to retain information when saving an image
|
||||
// FIXME: Should use a proper cache
|
||||
var photos map[string]*Photo
|
||||
|
||||
func init() {
|
||||
photos = make(map[string]*Photo)
|
||||
}
|
||||
|
||||
func doGet(url string, result interface{}) (err error) {
|
||||
req, err := http.NewRequest("GET", "https://api.unsplash.com/"+url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Client-ID "+config.BackgroundsUnsplashAccessToken.GetString())
|
||||
hc := http.Client{}
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(result)
|
||||
return
|
||||
}
|
||||
|
||||
func getImageID(fullURL string) string {
|
||||
// Unsplash image urls have the form
|
||||
// https://images.unsplash.com/photo-1590622878565-c662a7fd1394?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjcyODAwfQ
|
||||
// We only need the "photo-*" part of it.
|
||||
parts := strings.Split(strings.Split(fullURL, "?")[0], "photo-")
|
||||
if len(parts) < 2 {
|
||||
log.Errorf("Unsplash thumb url does not contain enough parts [parts: %v]", parts)
|
||||
return ""
|
||||
}
|
||||
return parts[1]
|
||||
}
|
||||
|
||||
// Search is the implementation to search on unsplash
|
||||
// @Summary Search for a background from unsplash
|
||||
// @Description Search for a list background from unsplash
|
||||
// @tags list
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param s query string false "Search backgrounds from unsplash with this search term."
|
||||
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
|
||||
// @Success 200 {array} background.Image "An array with photos"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /backgrounds/unsplash/search [get]
|
||||
func (p *Provider) Search(search string, page int64) (result []*background.Image, err error) {
|
||||
|
||||
// If we don't have a search query, return results from the unsplash featured collection
|
||||
if search == "" {
|
||||
collectionResult := []*Photo{}
|
||||
err = doGet("collections/317099/photos?page="+strconv.FormatInt(page, 10)+"&per_page=25&order_by=latest", &collectionResult)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result = []*background.Image{}
|
||||
for _, p := range collectionResult {
|
||||
result = append(result, &background.Image{
|
||||
ID: p.ID,
|
||||
URL: getImageID(p.Urls.Raw),
|
||||
Info: &models.UnsplashPhoto{
|
||||
UnsplashID: p.ID,
|
||||
Author: p.User.Username,
|
||||
AuthorName: p.User.Name,
|
||||
},
|
||||
})
|
||||
photos[p.ID] = p
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
searchResult := &SearchResult{}
|
||||
err = doGet("search/photos?query="+search+"&page="+strconv.FormatInt(page, 10)+"&per_page=25", &searchResult)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result = []*background.Image{}
|
||||
for _, p := range searchResult.Results {
|
||||
result = append(result, &background.Image{
|
||||
ID: p.ID,
|
||||
URL: getImageID(p.Urls.Raw),
|
||||
Info: &models.UnsplashPhoto{
|
||||
UnsplashID: p.ID,
|
||||
Author: p.User.Username,
|
||||
AuthorName: p.User.Name,
|
||||
},
|
||||
})
|
||||
photos[p.ID] = p
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Set sets an unsplash photo as list background
|
||||
// @Summary Set an unsplash photo as list background
|
||||
// @Description Sets a photo from unsplash as list background.1
|
||||
// @tags list
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param id path int true "List ID"
|
||||
// @Param list body background.Image true "The image you want to set as background"
|
||||
// @Success 200 {object} models.List "The background has been successfully set."
|
||||
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid image object provided."
|
||||
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the list"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /lists/{id}/backgrounds/unsplash [post]
|
||||
func (p *Provider) Set(image *background.Image, list *models.List, auth web.Auth) (err error) {
|
||||
|
||||
// Find the photo
|
||||
var photo *Photo
|
||||
var exists bool
|
||||
photo, exists = photos[image.ID]
|
||||
if !exists {
|
||||
log.Debugf("Image information for unsplash photo %s not cached, requesting from unsplash...", image.ID)
|
||||
photo = &Photo{}
|
||||
err = doGet("photos/"+image.ID, photo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Download the photo from unsplash
|
||||
// The parameters crop the image to a max width of 2560 and a max height of 2048 to save bandwidth and storage.
|
||||
resp, err := http.Get(photo.Urls.Raw + "&w=2560&h=2048&q=90")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode > 399 {
|
||||
b := bytes.Buffer{}
|
||||
_, _ = b.ReadFrom(resp.Body)
|
||||
log.Errorf("Error getting unsplash photo %s: Request failed with status %d, message was %s", photo.ID, resp.StatusCode, b.String())
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("Downloaded Unsplash Photo %s", image.ID)
|
||||
|
||||
// Save it as a file in vikunja
|
||||
file, err := files.Create(resp.Body, "", 0, auth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the old background if one exists
|
||||
if list.BackgroundFileID != 0 {
|
||||
file := files.File{ID: list.BackgroundFileID}
|
||||
if err := file.Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := models.RemoveUnsplashPhoto(list.BackgroundFileID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save the relation that we got it from unsplash
|
||||
unsplashPhoto := &models.UnsplashPhoto{
|
||||
FileID: file.ID,
|
||||
UnsplashID: image.ID,
|
||||
Author: photo.User.Username,
|
||||
AuthorName: photo.User.Name,
|
||||
}
|
||||
err = unsplashPhoto.Save()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Debugf("Saved unsplash photo %s as file %d with new entry %d", image.ID, file.ID, unsplashPhoto.ID)
|
||||
|
||||
// Set the file in the list
|
||||
list.BackgroundFileID = file.ID
|
||||
list.BackgroundInformation = unsplashPhoto
|
||||
|
||||
// Set it as the list background
|
||||
return models.SetListBackground(list.ID, file)
|
||||
}
|
||||
|
||||
// Pingback pings the unsplash api if an unsplash photo has been accessed.
|
||||
func Pingback(f *files.File) {
|
||||
// Check if the file is actually downloaded from unsplash
|
||||
unsplashPhoto, err := models.GetUnsplashPhotoByFileID(f.ID)
|
||||
if err != nil {
|
||||
log.Errorf("Unsplash Pingback: %s", err.Error())
|
||||
}
|
||||
|
||||
// Do the ping
|
||||
if _, err := http.Get("https://views.unsplash.com/v?app_id=" + config.BackgroundsUnsplashApplicationID.GetString() + "&photo_id=" + unsplashPhoto.UnsplashID); err != nil {
|
||||
log.Errorf("Unsplash Pingback Failed: %s", err.Error())
|
||||
}
|
||||
log.Debugf("Pinged unsplash for photo %s", unsplashPhoto.UnsplashID)
|
||||
}
|
|
@ -30,6 +30,8 @@ func InsertFromStructure(str []*models.NamespaceWithLists, user *user.User) (err
|
|||
|
||||
log.Debugf("[creating structure] Creating %d namespaces", len(str))
|
||||
|
||||
labels := make(map[string]*models.Label)
|
||||
|
||||
// Create all namespaces
|
||||
for _, n := range str {
|
||||
err = n.Create(user)
|
||||
|
@ -118,6 +120,34 @@ func InsertFromStructure(str []*models.NamespaceWithLists, user *user.User) (err
|
|||
log.Debugf("[creating structure] Created new attachment %d", a.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Create all labels
|
||||
for _, label := range t.Labels {
|
||||
// Check if we already have a label with that name + color combination and use it
|
||||
// If not, create one and save it for later
|
||||
var lb *models.Label
|
||||
var exists bool
|
||||
lb, exists = labels[label.Title+label.HexColor]
|
||||
if !exists {
|
||||
err = label.Create(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("[creating structure] Created new label %d", label.ID)
|
||||
labels[label.Title+label.HexColor] = label
|
||||
lb = label
|
||||
}
|
||||
|
||||
lt := &models.LabelTask{
|
||||
LabelID: lb.ID,
|
||||
TaskID: t.ID,
|
||||
}
|
||||
err = lt.Create(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
testStructure := []*models.NamespaceWithLists{
|
||||
{
|
||||
Namespace: models.Namespace{
|
||||
Name: "Test1",
|
||||
Title: "Test1",
|
||||
Description: "Lorem Ipsum",
|
||||
},
|
||||
Lists: []*models.List{
|
||||
|
@ -43,22 +43,22 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
Description: "Something",
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Text: "Task1",
|
||||
Title: "Task1",
|
||||
Description: "Lorem",
|
||||
},
|
||||
{
|
||||
Text: "Task with related tasks",
|
||||
Title: "Task with related tasks",
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindSubtask: {
|
||||
{
|
||||
Text: "Related to task with related task",
|
||||
Title: "Related to task with related task",
|
||||
Description: "As subtask",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Text: "Task with attachments",
|
||||
Title: "Task with attachments",
|
||||
Attachments: []*models.TaskAttachment{
|
||||
{
|
||||
File: &files.File{
|
||||
|
@ -69,6 +69,28 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task with labels",
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label1",
|
||||
HexColor: "ff00ff",
|
||||
},
|
||||
{
|
||||
Title: "Label2",
|
||||
HexColor: "ff00ff",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task with same label",
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
Title: "Label1",
|
||||
HexColor: "ff00ff",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -30,7 +30,7 @@ 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
|
||||
// Name holds the name of the migration.
|
||||
// Title 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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,476 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package todoist
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/migration"
|
||||
"code.vikunja.io/api/pkg/timeutil"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Migration is the todoist migration struct
|
||||
type Migration struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type apiTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
type label struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color int `json:"color"`
|
||||
ItemOrder int `json:"item_order"`
|
||||
IsDeleted int `json:"is_deleted"`
|
||||
IsFavorite int `json:"is_favorite"`
|
||||
}
|
||||
|
||||
type project struct {
|
||||
ID int `json:"id"`
|
||||
LegacyID int `json:"legacy_id"`
|
||||
Name string `json:"name"`
|
||||
Color int `json:"color"`
|
||||
ParentID int `json:"parent_id"`
|
||||
ChildOrder int `json:"child_order"`
|
||||
Collapsed int `json:"collapsed"`
|
||||
Shared bool `json:"shared"`
|
||||
LegacyParentID int `json:"legacy_parent_id"`
|
||||
SyncID int `json:"sync_id"`
|
||||
IsDeleted int `json:"is_deleted"`
|
||||
IsArchived int `json:"is_archived"`
|
||||
IsFavorite int `json:"is_favorite"`
|
||||
}
|
||||
|
||||
type dueDate struct {
|
||||
Date string `json:"date"`
|
||||
Timezone interface{} `json:"timezone"`
|
||||
String string `json:"string"`
|
||||
Lang string `json:"lang"`
|
||||
IsRecurring bool `json:"is_recurring"`
|
||||
}
|
||||
|
||||
type item struct {
|
||||
ID int `json:"id"`
|
||||
LegacyID int `json:"legacy_id"`
|
||||
UserID int `json:"user_id"`
|
||||
ProjectID int `json:"project_id"`
|
||||
LegacyProjectID int `json:"legacy_project_id"`
|
||||
Content string `json:"content"`
|
||||
Priority int `json:"priority"`
|
||||
Due *dueDate `json:"due"`
|
||||
ParentID int `json:"parent_id"`
|
||||
LegacyParentID int `json:"legacy_parent_id"`
|
||||
ChildOrder int `json:"child_order"`
|
||||
SectionID int `json:"section_id"`
|
||||
DayOrder int `json:"day_order"`
|
||||
Collapsed int `json:"collapsed"`
|
||||
Children interface{} `json:"children"`
|
||||
Labels []int `json:"labels"`
|
||||
AddedByUID int `json:"added_by_uid"`
|
||||
AssignedByUID int `json:"assigned_by_uid"`
|
||||
ResponsibleUID int `json:"responsible_uid"`
|
||||
Checked int `json:"checked"`
|
||||
InHistory int `json:"in_history"`
|
||||
IsDeleted int `json:"is_deleted"`
|
||||
DateAdded time.Time `json:"date_added"`
|
||||
HasMoreNotes bool `json:"has_more_notes"`
|
||||
DateCompleted time.Time `json:"date_completed"`
|
||||
}
|
||||
|
||||
type fileAttachment struct {
|
||||
FileType string `json:"file_type"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int `json:"file_size"`
|
||||
FileURL string `json:"file_url"`
|
||||
UploadState string `json:"upload_state"`
|
||||
}
|
||||
|
||||
type note struct {
|
||||
ID int `json:"id"`
|
||||
LegacyID int `json:"legacy_id"`
|
||||
PostedUID int `json:"posted_uid"`
|
||||
ProjectID int `json:"project_id"`
|
||||
LegacyProjectID int `json:"legacy_project_id"`
|
||||
ItemID int `json:"item_id"`
|
||||
LegacyItemID int `json:"legacy_item_id"`
|
||||
Content string `json:"content"`
|
||||
FileAttachment *fileAttachment `json:"file_attachment"`
|
||||
UidsToNotify []int `json:"uids_to_notify"`
|
||||
IsDeleted int `json:"is_deleted"`
|
||||
Posted time.Time `json:"posted"`
|
||||
}
|
||||
|
||||
type projectNote struct {
|
||||
Content string `json:"content"`
|
||||
FileAttachment *fileAttachment `json:"file_attachment"`
|
||||
ID int64 `json:"id"`
|
||||
IsDeleted int `json:"is_deleted"`
|
||||
Posted time.Time `json:"posted"`
|
||||
PostedUID int `json:"posted_uid"`
|
||||
ProjectID int `json:"project_id"`
|
||||
UidsToNotify []int `json:"uids_to_notify"`
|
||||
}
|
||||
|
||||
type reminder struct {
|
||||
ID int `json:"id"`
|
||||
NotifyUID int `json:"notify_uid"`
|
||||
ItemID int `json:"item_id"`
|
||||
Service string `json:"service"`
|
||||
Type string `json:"type"`
|
||||
Due *dueDate `json:"due"`
|
||||
MmOffset int `json:"mm_offset"`
|
||||
IsDeleted int `json:"is_deleted"`
|
||||
}
|
||||
|
||||
type sync struct {
|
||||
Projects []*project `json:"projects"`
|
||||
Items []*item `json:"items"`
|
||||
Labels []*label `json:"labels"`
|
||||
Notes []*note `json:"notes"`
|
||||
ProjectNotes []*projectNote `json:"project_notes"`
|
||||
Reminders []*reminder `json:"reminders"`
|
||||
}
|
||||
|
||||
var todoistColors = map[int]string{}
|
||||
|
||||
func init() {
|
||||
todoistColors = make(map[int]string, 19)
|
||||
// The todoists colors are static, taken from https://developer.todoist.com/sync/v8/#colors
|
||||
todoistColors = map[int]string{
|
||||
30: "b8256f",
|
||||
31: "db4035",
|
||||
32: "ff9933",
|
||||
33: "fad000",
|
||||
34: "afb83b",
|
||||
35: "7ecc49",
|
||||
36: "299438",
|
||||
37: "6accbc",
|
||||
38: "158fad",
|
||||
39: "14aaf5",
|
||||
40: "96c3eb",
|
||||
41: "4073ff",
|
||||
42: "884dff",
|
||||
43: "af38eb",
|
||||
44: "eb96eb",
|
||||
45: "e05194",
|
||||
46: "ff8d85",
|
||||
47: "808080",
|
||||
48: "b8b8b8",
|
||||
49: "ccac93",
|
||||
}
|
||||
}
|
||||
|
||||
// Name is used to get the name of the todoist migration - we're using the docs here to annotate the status route.
|
||||
// @Summary Get migration status
|
||||
// @Description Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.
|
||||
// @tags migration
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} migration.Status "The migration status"
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
// @Router /migration/todoist/status [get]
|
||||
func (m *Migration) Name() string {
|
||||
return "todoist"
|
||||
}
|
||||
|
||||
// AuthURL returns the url users need to authenticate against
|
||||
// @Summary Get the auth url from todoist
|
||||
// @Description Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from todoist to Vikunja.
|
||||
// @tags migration
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Success 200 {object} handler.AuthURL "The auth url."
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
// @Router /migration/todoist/auth [get]
|
||||
func (m *Migration) AuthURL() string {
|
||||
return "https://todoist.com/oauth/authorize" +
|
||||
"?client_id=" + config.MigrationTodoistClientID.GetString() +
|
||||
"&scope=data:read" +
|
||||
"&state=" + utils.MakeRandomString(32)
|
||||
}
|
||||
|
||||
func doPost(url string, form url.Values) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
hc := http.Client{}
|
||||
return hc.Do(req)
|
||||
}
|
||||
|
||||
func convertTodoistToVikunja(sync *sync) (fullVikunjaHierachie []*models.NamespaceWithLists, err error) {
|
||||
|
||||
newNamespace := &models.NamespaceWithLists{
|
||||
Namespace: models.Namespace{
|
||||
Title: "Migrated from todoist",
|
||||
},
|
||||
}
|
||||
|
||||
// A map for all vikunja lists with the project id they're coming from as key
|
||||
lists := make(map[int]*models.List, len(sync.Projects))
|
||||
|
||||
// A map for all vikunja tasks with the todoist task id as key to find them easily and add more data
|
||||
tasks := make(map[int]*models.Task, len(sync.Items))
|
||||
|
||||
// A map for all vikunja labels with the todoist id as key to find them easier
|
||||
labels := make(map[int]*models.Label, len(sync.Labels))
|
||||
|
||||
for _, p := range sync.Projects {
|
||||
list := &models.List{
|
||||
Title: p.Name,
|
||||
HexColor: todoistColors[p.Color],
|
||||
IsArchived: p.IsArchived == 1,
|
||||
}
|
||||
|
||||
lists[p.ID] = list
|
||||
|
||||
newNamespace.Lists = append(newNamespace.Lists, list)
|
||||
}
|
||||
|
||||
for _, label := range sync.Labels {
|
||||
labels[label.ID] = &models.Label{
|
||||
Title: label.Name,
|
||||
HexColor: todoistColors[label.Color],
|
||||
}
|
||||
}
|
||||
|
||||
for _, i := range sync.Items {
|
||||
task := &models.Task{
|
||||
Title: i.Content,
|
||||
Created: timeutil.FromTime(i.DateAdded),
|
||||
Done: i.Checked == 1,
|
||||
}
|
||||
|
||||
// Only try to parse the task done at date if the task is actually done
|
||||
// Sometimes weired things happen if we try to parse nil dates.
|
||||
if task.Done {
|
||||
task.DoneAt = timeutil.FromTime(i.DateCompleted)
|
||||
}
|
||||
|
||||
// Todoist priorities only range from 1 (lowest) and max 4 (highest), so we need to make slight adjustments
|
||||
if i.Priority > 1 {
|
||||
task.Priority = int64(i.Priority)
|
||||
}
|
||||
|
||||
// Put the due date together
|
||||
if i.Due != nil {
|
||||
dueDate, err := time.Parse("2006-01-02", i.Due.Date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task.DueDate = timeutil.FromTime(dueDate)
|
||||
}
|
||||
|
||||
// Put all labels together from earlier
|
||||
for _, lID := range i.Labels {
|
||||
task.Labels = append(task.Labels, labels[lID])
|
||||
}
|
||||
|
||||
tasks[i.ID] = task
|
||||
|
||||
lists[i.ProjectID].Tasks = append(lists[i.ProjectID].Tasks, task)
|
||||
}
|
||||
|
||||
// If the parenId of a task is not 0, create a task relation
|
||||
// We're looping again here to make sure we have seem all tasks before and have them in our map
|
||||
for _, i := range sync.Items {
|
||||
if i.ParentID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prevent all those nil errors
|
||||
if tasks[i.ParentID].RelatedTasks == nil {
|
||||
tasks[i.ParentID].RelatedTasks = make(models.RelatedTaskMap)
|
||||
}
|
||||
|
||||
tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask] = append(tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask], tasks[i.ID])
|
||||
}
|
||||
|
||||
// Task Notes -> Task Descriptions
|
||||
for _, n := range sync.Notes {
|
||||
if tasks[n.ItemID].Description != "" {
|
||||
tasks[n.ItemID].Description += "\n"
|
||||
}
|
||||
tasks[n.ItemID].Description += n.Content
|
||||
|
||||
if n.FileAttachment == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Download the attachment and put it in the file
|
||||
resp, err := http.Get(n.FileAttachment.FileURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
buf := &bytes.Buffer{}
|
||||
_, err = buf.ReadFrom(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tasks[n.ItemID].Attachments = append(tasks[n.ItemID].Attachments, &models.TaskAttachment{
|
||||
File: &files.File{
|
||||
Name: n.FileAttachment.FileName,
|
||||
Mime: n.FileAttachment.FileType,
|
||||
Size: uint64(n.FileAttachment.FileSize),
|
||||
Created: n.Posted,
|
||||
CreatedUnix: timeutil.FromTime(n.Posted),
|
||||
// We directly pass the file contents here to have a way to link the attachment to the file later.
|
||||
// Because we don't have an ID for our task at this point of the migration, we cannot just throw all
|
||||
// attachments in a slice and do the work of downloading and properly storing them later.
|
||||
FileContent: buf.Bytes(),
|
||||
},
|
||||
Created: timeutil.FromTime(n.Posted),
|
||||
})
|
||||
}
|
||||
|
||||
// Project Notes -> List Descriptions
|
||||
for _, pn := range sync.ProjectNotes {
|
||||
if lists[pn.ProjectID].Description != "" {
|
||||
lists[pn.ProjectID].Description += "\n"
|
||||
}
|
||||
|
||||
lists[pn.ProjectID].Description += pn.Content
|
||||
}
|
||||
|
||||
// Reminders -> vikunja reminders
|
||||
for _, r := range sync.Reminders {
|
||||
if r.Due == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
date, err := time.Parse("2006-01-02", r.Due.Date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tasks[r.ItemID].Reminders = append(tasks[r.ItemID].Reminders, timeutil.FromTime(date))
|
||||
}
|
||||
|
||||
return []*models.NamespaceWithLists{
|
||||
newNamespace,
|
||||
}, err
|
||||
}
|
||||
|
||||
func getAccessTokenFromAuthToken(authToken string) (accessToken string, err error) {
|
||||
|
||||
form := url.Values{
|
||||
"client_id": []string{config.MigrationTodoistClientID.GetString()},
|
||||
"client_secret": []string{config.MigrationTodoistClientSecret.GetString()},
|
||||
"code": []string{authToken},
|
||||
"redirect_uri": []string{config.MigrationTodoistRedirectURL.GetString()},
|
||||
}
|
||||
resp, err := doPost("https://todoist.com/oauth/access_token", form)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode > 399 {
|
||||
buf := &bytes.Buffer{}
|
||||
_, _ = buf.ReadFrom(resp.Body)
|
||||
return "", fmt.Errorf("got http status %d while trying to get token, error was %s", resp.StatusCode, buf.String())
|
||||
}
|
||||
|
||||
token := &apiTokenResponse{}
|
||||
err = json.NewDecoder(resp.Body).Decode(token)
|
||||
return token.AccessToken, err
|
||||
}
|
||||
|
||||
// Migrate gets all tasks from todoist for a user and puts them into vikunja
|
||||
// @Summary Migrate all lists, tasks etc. from todoist
|
||||
// @Description Migrates all projects, tasks, notes, reminders, subtasks and files from todoist to vikunja.
|
||||
// @tags migration
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param migrationCode body todoist.Migration true "The auth code previously obtained from the auth url. See the docs for /migration/todoist/auth."
|
||||
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
|
||||
// @Failure 500 {object} models.Message "Internal server error"
|
||||
// @Router /migration/todoist/migrate [post]
|
||||
func (m *Migration) Migrate(u *user.User) (err error) {
|
||||
|
||||
log.Debugf("[Todoist Migration] Starting migration for user %d", u.ID)
|
||||
|
||||
// 0. Get an api token from the obtained auth token
|
||||
token, err := getAccessTokenFromAuthToken(m.Code)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
log.Debugf("[Todoist Migration] Could not get token")
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Todoist Migration] Got user token for user %d", u.ID)
|
||||
log.Debugf("[Todoist Migration] Getting todoist data for user %d", u.ID)
|
||||
|
||||
// Get everything with the sync api
|
||||
form := url.Values{
|
||||
"token": []string{token},
|
||||
"sync_token": []string{"*"},
|
||||
"resource_types": []string{"[\"all\"]"},
|
||||
}
|
||||
resp, err := doPost("https://api.todoist.com/sync/v8/sync", form)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
syncResponse := &sync{}
|
||||
err = json.NewDecoder(resp.Body).Decode(syncResponse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Todoist Migration] Got all todoist user data for user %d", u.ID)
|
||||
log.Debugf("[Todoist Migration] Start converting data for user %d", u.ID)
|
||||
|
||||
fullVikunjaHierachie, err := convertTodoistToVikunja(syncResponse)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Todoist Migration] Done converting data for user %d", u.ID)
|
||||
log.Debugf("[Todoist Migration] Start inserting data for user %d", u.ID)
|
||||
|
||||
err = migration.InsertFromStructure(fullVikunjaHierachie, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Todoist Migration] Done inserting data for user %d", u.ID)
|
||||
log.Debugf("[Todoist Migration] Todoist migration done for user %d", u.ID)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,550 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License 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 General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package todoist
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/timeutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/d4l3k/messagediff.v1"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConvertTodoistToVikunja(t *testing.T) {
|
||||
|
||||
config.InitConfig()
|
||||
|
||||
time1, err := time.Parse(time.RFC3339Nano, "2014-09-26T08:25:05Z")
|
||||
assert.NoError(t, err)
|
||||
time3, err := time.Parse(time.RFC3339Nano, "2014-10-21T08:25:05Z")
|
||||
assert.NoError(t, err)
|
||||
dueTime, err := time.Parse(time.RFC3339Nano, "2020-05-31T00:00:00Z")
|
||||
assert.NoError(t, err)
|
||||
nilTime, err := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00Z")
|
||||
assert.NoError(t, err)
|
||||
exampleFile, err := ioutil.ReadFile(config.ServiceRootpath.GetString() + "/pkg/modules/migration/wunderlist/testimage.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
makeTestItem := func(id, projectId int, hasDueDate, hasLabels, done bool) *item {
|
||||
item := &item{
|
||||
ID: id,
|
||||
UserID: 1855589,
|
||||
ProjectID: projectId,
|
||||
Content: "Task" + strconv.Itoa(id),
|
||||
Priority: 1,
|
||||
ParentID: 0,
|
||||
ChildOrder: 1,
|
||||
DateAdded: time1,
|
||||
DateCompleted: nilTime,
|
||||
}
|
||||
|
||||
if done {
|
||||
item.Checked = 1
|
||||
item.DateCompleted = time3
|
||||
}
|
||||
|
||||
if hasLabels {
|
||||
item.Labels = []int{
|
||||
80000,
|
||||
80001,
|
||||
80002,
|
||||
80003,
|
||||
}
|
||||
}
|
||||
|
||||
if hasDueDate {
|
||||
item.Due = &dueDate{
|
||||
Date: "2020-05-31",
|
||||
Timezone: nil,
|
||||
IsRecurring: false,
|
||||
}
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
testSync := &sync{
|
||||
Projects: []*project{
|
||||
{
|
||||
ID: 396936926,
|
||||
Name: "Project1",
|
||||
Color: 30,
|
||||
ChildOrder: 1,
|
||||
Collapsed: 0,
|
||||
Shared: false,
|
||||
IsDeleted: 0,
|
||||
IsArchived: 0,
|
||||
IsFavorite: 0,
|
||||
},
|
||||
{
|
||||
ID: 396936927,
|
||||
Name: "Project2",
|
||||
Color: 37,
|
||||
ChildOrder: 1,
|
||||
Collapsed: 0,
|
||||
Shared: false,
|
||||
IsDeleted: 0,
|
||||
IsArchived: 0,
|
||||
IsFavorite: 0,
|
||||
},
|
||||
{
|
||||
ID: 396936928,
|
||||
Name: "Project3 - Archived",
|
||||
Color: 37,
|
||||
ChildOrder: 1,
|
||||
Collapsed: 0,
|
||||
Shared: false,
|
||||
IsDeleted: 0,
|
||||
IsArchived: 1,
|
||||
IsFavorite: 0,
|
||||
},
|
||||
},
|
||||
Items: []*item{
|
||||
makeTestItem(400000000, 396936926, false, false, false),
|
||||
makeTestItem(400000001, 396936926, false, false, false),
|
||||
makeTestItem(400000002, 396936926, false, false, false),
|
||||
makeTestItem(400000003, 396936926, true, true, true),
|
||||
makeTestItem(400000004, 396936926, false, true, false),
|
||||
makeTestItem(400000005, 396936926, true, false, true),
|
||||
makeTestItem(400000006, 396936926, true, false, true),
|
||||
{
|
||||
ID: 400000110,
|
||||
UserID: 1855589,
|
||||
ProjectID: 396936926,
|
||||
Content: "Task with parent",
|
||||
Priority: 2,
|
||||
ParentID: 400000006,
|
||||
ChildOrder: 1,
|
||||
Checked: 0,
|
||||
DateAdded: time1,
|
||||
},
|
||||
makeTestItem(400000106, 396936926, true, true, true),
|
||||
makeTestItem(400000107, 396936926, false, false, true),
|
||||
makeTestItem(400000108, 396936926, false, false, true),
|
||||
makeTestItem(400000109, 396936926, false, false, true),
|
||||
|
||||
makeTestItem(400000007, 396936927, true, false, false),
|
||||
makeTestItem(400000008, 396936927, true, false, false),
|
||||
makeTestItem(400000009, 396936927, false, false, false),
|
||||
makeTestItem(400000010, 396936927, false, false, true),
|
||||
makeTestItem(400000101, 396936927, false, false, false),
|
||||
makeTestItem(400000102, 396936927, true, true, false),
|
||||
makeTestItem(400000103, 396936927, false, true, false),
|
||||
makeTestItem(400000104, 396936927, false, true, false),
|
||||
makeTestItem(400000105, 396936927, true, true, false),
|
||||
|
||||
makeTestItem(400000111, 396936928, false, false, true),
|
||||
},
|
||||
Labels: []*label{
|
||||
{
|
||||
ID: 80000,
|
||||
Name: "Label1",
|
||||
Color: 30,
|
||||
},
|
||||
{
|
||||
ID: 80001,
|
||||
Name: "Label2",
|
||||
Color: 31,
|
||||
},
|
||||
{
|
||||
ID: 80002,
|
||||
Name: "Label3",
|
||||
Color: 32,
|
||||
},
|
||||
{
|
||||
ID: 80003,
|
||||
Name: "Label4",
|
||||
Color: 33,
|
||||
},
|
||||
},
|
||||
Notes: []*note{
|
||||
{
|
||||
ID: 101476,
|
||||
PostedUID: 1855589,
|
||||
ItemID: 400000000,
|
||||
Content: "Lorem Ipsum dolor sit amet",
|
||||
Posted: time1,
|
||||
},
|
||||
{
|
||||
ID: 101477,
|
||||
PostedUID: 1855589,
|
||||
ItemID: 400000001,
|
||||
Content: "Lorem Ipsum dolor sit amet",
|
||||
Posted: time1,
|
||||
},
|
||||
{
|
||||
ID: 101478,
|
||||
PostedUID: 1855589,
|
||||
ItemID: 400000003,
|
||||
Content: "Lorem Ipsum dolor sit amet",
|
||||
Posted: time1,
|
||||
},
|
||||
{
|
||||
ID: 101479,
|
||||
PostedUID: 1855589,
|
||||
ItemID: 400000010,
|
||||
Content: "Lorem Ipsum dolor sit amet",
|
||||
Posted: time1,
|
||||
},
|
||||
{
|
||||
ID: 101480,
|
||||
PostedUID: 1855589,
|
||||
ItemID: 400000101,
|
||||
Content: "Lorem Ipsum dolor sit amet",
|
||||
FileAttachment: &fileAttachment{
|
||||
FileName: "file.md",
|
||||
FileType: "text/plain",
|
||||
FileSize: 12345,
|
||||
FileURL: "https://vikunja.io/testimage.jpg", // Using an image which we are hosting, so it'll still be up
|
||||
UploadState: "completed",
|
||||
},
|
||||
Posted: time1,
|
||||
},
|
||||
},
|
||||
ProjectNotes: []*projectNote{
|
||||
{
|
||||
ID: 102000,
|
||||
Content: "Lorem Ipsum dolor sit amet",
|
||||
ProjectID: 396936926,
|
||||
Posted: time3,
|
||||
PostedUID: 1855589,
|
||||
},
|
||||
{
|
||||
ID: 102001,
|
||||
Content: "Lorem Ipsum dolor sit amet 2",
|
||||
ProjectID: 396936926,
|
||||
Posted: time3,
|
||||
PostedUID: 1855589,
|
||||
},
|
||||
{
|
||||
ID: 102002,
|
||||
Content: "Lorem Ipsum dolor sit amet 3",
|
||||
ProjectID: 396936926,
|
||||
Posted: time3,
|
||||
PostedUID: 1855589,
|
||||
},
|
||||
{
|
||||
ID: 102003,
|
||||
Content: "Lorem Ipsum dolor sit amet 4",
|
||||
ProjectID: 396936927,
|
||||
Posted: time3,
|
||||
PostedUID: 1855589,
|
||||
},
|
||||
{
|
||||
ID: 102004,
|
||||
Content: "Lorem Ipsum dolor sit amet 5",
|
||||
ProjectID: 396936927,
|
||||
Posted: time3,
|
||||
PostedUID: 1855589,
|
||||
},
|
||||
},
|
||||
Reminders: []*reminder{
|
||||
{
|
||||
ID: 103000,
|
||||
ItemID: 400000000,
|
||||
Due: &dueDate{
|
||||
Date: "2020-06-15",
|
||||
IsRecurring: false,
|
||||
},
|
||||
MmOffset: 180,
|
||||
},
|
||||
{
|
||||
ID: 103001,
|
||||
ItemID: 400000000,
|
||||
Due: &dueDate{
|
||||
Date: "2020-06-16",
|
||||
IsRecurring: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 103002,
|
||||
ItemID: 400000002,
|
||||
Due: &dueDate{
|
||||
Date: "2020-07-15",
|
||||
IsRecurring: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 103003,
|
||||
ItemID: 400000003,
|
||||
Due: &dueDate{
|
||||
Date: "2020-06-15",
|
||||
IsRecurring: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 103004,
|
||||
ItemID: 400000005,
|
||||
Due: &dueDate{
|
||||
Date: "2020-06-15",
|
||||
IsRecurring: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 103006,
|
||||
ItemID: 400000009,
|
||||
Due: &dueDate{
|
||||
Date: "2020-06-15",
|
||||
IsRecurring: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
vikunjaLabels := []*models.Label{
|
||||
{
|
||||
Title: "Label1",
|
||||
HexColor: todoistColors[30],
|
||||
},
|
||||
{
|
||||
Title: "Label2",
|
||||
HexColor: todoistColors[31],
|
||||
},
|
||||
{
|
||||
Title: "Label3",
|
||||
HexColor: todoistColors[32],
|
||||
},
|
||||
{
|
||||
Title: "Label4",
|
||||
HexColor: todoistColors[33],
|
||||
},
|
||||
}
|
||||
|
||||
expectedHierachie := []*models.NamespaceWithLists{
|
||||
{
|
||||
Namespace: models.Namespace{
|
||||
Title: "Migrated from todoist",
|
||||
},
|
||||
Lists: []*models.List{
|
||||
{
|
||||
Title: "Project1",
|
||||
Description: "Lorem Ipsum dolor sit amet\nLorem Ipsum dolor sit amet 2\nLorem Ipsum dolor sit amet 3",
|
||||
HexColor: todoistColors[30],
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Title: "Task400000000",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Reminders: []timeutil.TimeStamp{
|
||||
timeutil.FromTime(time.Date(2020, time.June, 15, 0, 0, 0, 0, time.UTC)),
|
||||
timeutil.FromTime(time.Date(2020, time.June, 16, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task400000001",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
},
|
||||
{
|
||||
Title: "Task400000002",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Reminders: []timeutil.TimeStamp{
|
||||
timeutil.FromTime(time.Date(2020, time.July, 15, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task400000003",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Done: true,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
Labels: vikunjaLabels,
|
||||
Reminders: []timeutil.TimeStamp{
|
||||
timeutil.FromTime(time.Date(2020, time.June, 15, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task400000004",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
{
|
||||
Title: "Task400000005",
|
||||
Done: true,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
Reminders: []timeutil.TimeStamp{
|
||||
timeutil.FromTime(time.Date(2020, time.June, 15, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task400000006",
|
||||
Done: true,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindSubtask: {
|
||||
{
|
||||
Title: "Task with parent",
|
||||
Done: false,
|
||||
Priority: 2,
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(nilTime),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task with parent",
|
||||
Done: false,
|
||||
Priority: 2,
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(nilTime),
|
||||
},
|
||||
{
|
||||
Title: "Task400000106",
|
||||
Done: true,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
{
|
||||
Title: "Task400000107",
|
||||
Done: true,
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
},
|
||||
{
|
||||
Title: "Task400000108",
|
||||
Done: true,
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
},
|
||||
{
|
||||
Title: "Task400000109",
|
||||
Done: true,
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Project2",
|
||||
Description: "Lorem Ipsum dolor sit amet 4\nLorem Ipsum dolor sit amet 5",
|
||||
HexColor: todoistColors[37],
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Title: "Task400000007",
|
||||
Done: false,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
},
|
||||
{
|
||||
Title: "Task400000008",
|
||||
Done: false,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
},
|
||||
{
|
||||
Title: "Task400000009",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Reminders: []timeutil.TimeStamp{
|
||||
timeutil.FromTime(time.Date(2020, time.June, 15, 0, 0, 0, 0, time.UTC)),
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task400000010",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Done: true,
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
},
|
||||
{
|
||||
Title: "Task400000101",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Attachments: []*models.TaskAttachment{
|
||||
{
|
||||
File: &files.File{
|
||||
Name: "file.md",
|
||||
Mime: "text/plain",
|
||||
Size: 12345,
|
||||
Created: time1,
|
||||
CreatedUnix: timeutil.FromTime(time1),
|
||||
FileContent: exampleFile,
|
||||
},
|
||||
Created: timeutil.FromTime(time1),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Task400000102",
|
||||
Done: false,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
{
|
||||
Title: "Task400000103",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
{
|
||||
Title: "Task400000104",
|
||||
Done: false,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
{
|
||||
Title: "Task400000105",
|
||||
Done: false,
|
||||
DueDate: timeutil.FromTime(dueTime),
|
||||
Created: timeutil.FromTime(time1),
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Title: "Project3 - Archived",
|
||||
HexColor: todoistColors[37],
|
||||
IsArchived: true,
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Title: "Task400000111",
|
||||
Done: true,
|
||||
Created: timeutil.FromTime(time1),
|
||||
DoneAt: timeutil.FromTime(time3),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hierachie, err := convertTodoistToVikunja(testSync)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, hierachie)
|
||||
if diff, equal := messagediff.PrettyDiff(hierachie, expectedHierachie); !equal {
|
||||
t.Errorf("converted todoist data = %v, want %v, diff: %v", hierachie, expectedHierachie, diff)
|
||||
}
|
||||
}
|
|
@ -152,7 +152,7 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
|
|||
for _, t := range content.tasks {
|
||||
if t.ListID == listID {
|
||||
newTask := &models.Task{
|
||||
Text: t.Title,
|
||||
Title: t.Title,
|
||||
Created: timeutil.FromTime(t.CreatedAt),
|
||||
Done: t.Completed,
|
||||
}
|
||||
|
@ -217,7 +217,7 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
|
|||
newTask.RelatedTasks = make(models.RelatedTaskMap)
|
||||
}
|
||||
newTask.RelatedTasks[models.RelationKindSubtask] = append(newTask.RelatedTasks[models.RelationKindSubtask], &models.Task{
|
||||
Text: s.Title,
|
||||
Title: s.Title,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +247,7 @@ func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierach
|
|||
for _, folder := range content.folders {
|
||||
namespace := &models.NamespaceWithLists{
|
||||
Namespace: models.Namespace{
|
||||
Name: folder.Title,
|
||||
Title: folder.Title,
|
||||
Created: timeutil.FromTime(folder.CreatedAt),
|
||||
Updated: timeutil.FromTime(folder.UpdatedAt),
|
||||
},
|
||||
|
@ -274,7 +274,7 @@ func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierach
|
|||
if len(listMap) > 0 {
|
||||
newNamespace := &models.NamespaceWithLists{
|
||||
Namespace: models.Namespace{
|
||||
Name: "Migrated from wunderlist",
|
||||
Title: "Migrated from wunderlist",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -192,7 +192,7 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
expectedHierachie := []*models.NamespaceWithLists{
|
||||
{
|
||||
Namespace: models.Namespace{
|
||||
Name: "Lorem Ipsum",
|
||||
Title: "Lorem Ipsum",
|
||||
Created: timeutil.FromTime(time1),
|
||||
Updated: timeutil.FromTime(time2),
|
||||
},
|
||||
|
@ -202,7 +202,7 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
Title: "Lorem1",
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Text: "Ipsum1",
|
||||
Title: "Ipsum1",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
|
@ -222,17 +222,17 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
Reminders: []timeutil.TimeStamp{timeutil.FromTime(time4)},
|
||||
},
|
||||
{
|
||||
Text: "Ipsum2",
|
||||
Title: "Ipsum2",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindSubtask: {
|
||||
{
|
||||
Text: "LoremSub1",
|
||||
Title: "LoremSub1",
|
||||
},
|
||||
{
|
||||
Text: "LoremSub2",
|
||||
Title: "LoremSub2",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -244,7 +244,7 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
Title: "Lorem2",
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Text: "Ipsum3",
|
||||
Title: "Ipsum3",
|
||||
Done: true,
|
||||
DoneAt: timeutil.FromTime(time1),
|
||||
DueDate: 1378339200,
|
||||
|
@ -265,14 +265,14 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
Text: "Ipsum4",
|
||||
Title: "Ipsum4",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Reminders: []timeutil.TimeStamp{timeutil.FromTime(time3)},
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindSubtask: {
|
||||
{
|
||||
Text: "LoremSub3",
|
||||
Title: "LoremSub3",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -284,26 +284,26 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
Title: "Lorem3",
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Text: "Ipsum5",
|
||||
Title: "Ipsum5",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
},
|
||||
{
|
||||
Text: "Ipsum6",
|
||||
Title: "Ipsum6",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Done: true,
|
||||
DoneAt: timeutil.FromTime(time1),
|
||||
},
|
||||
{
|
||||
Text: "Ipsum7",
|
||||
Title: "Ipsum7",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Done: true,
|
||||
DoneAt: timeutil.FromTime(time1),
|
||||
},
|
||||
{
|
||||
Text: "Ipsum8",
|
||||
Title: "Ipsum8",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
},
|
||||
|
@ -314,14 +314,14 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
Title: "Lorem4",
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Text: "Ipsum9",
|
||||
Title: "Ipsum9",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Done: true,
|
||||
DoneAt: timeutil.FromTime(time1),
|
||||
},
|
||||
{
|
||||
Text: "Ipsum10",
|
||||
Title: "Ipsum10",
|
||||
DueDate: 1378339200,
|
||||
Created: timeutil.FromTime(time1),
|
||||
Done: true,
|
||||
|
@ -333,7 +333,7 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
},
|
||||
{
|
||||
Namespace: models.Namespace{
|
||||
Name: "Migrated from wunderlist",
|
||||
Title: "Migrated from wunderlist",
|
||||
},
|
||||
Lists: []*models.List{
|
||||
{
|
||||
|
@ -348,6 +348,6 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.NotNil(t, hierachie)
|
||||
if diff, equal := messagediff.PrettyDiff(hierachie, expectedHierachie); !equal {
|
||||
t.Errorf("ListUser.ReadAll() = %v, want %v, diff: %v", hierachie, expectedHierachie, diff)
|
||||
t.Errorf("converted wunderlist data = %v, want %v, diff: %v", hierachie, expectedHierachie, diff)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -18,20 +18,24 @@ package v1
|
|||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/modules/migration/todoist"
|
||||
"code.vikunja.io/api/pkg/modules/migration/wunderlist"
|
||||
"code.vikunja.io/api/pkg/version"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type vikunjaInfos struct {
|
||||
Version string `json:"version"`
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
Motd string `json:"motd"`
|
||||
LinkSharingEnabled bool `json:"link_sharing_enabled"`
|
||||
MaxFileSize string `json:"max_file_size"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
AvailableMigrators []string `json:"available_migrators"`
|
||||
TaskAttachmentsEnabled bool `json:"task_attachments_enabled"`
|
||||
Version string `json:"version"`
|
||||
FrontendURL string `json:"frontend_url"`
|
||||
Motd string `json:"motd"`
|
||||
LinkSharingEnabled bool `json:"link_sharing_enabled"`
|
||||
MaxFileSize string `json:"max_file_size"`
|
||||
RegistrationEnabled bool `json:"registration_enabled"`
|
||||
AvailableMigrators []string `json:"available_migrators"`
|
||||
TaskAttachmentsEnabled bool `json:"task_attachments_enabled"`
|
||||
EnabledBackgroundProviders []string `json:"enabled_background_providers"`
|
||||
TotpEnabled bool `json:"totp_enabled"`
|
||||
}
|
||||
|
||||
// Info is the handler to get infos about this vikunja instance
|
||||
|
@ -42,7 +46,7 @@ type vikunjaInfos struct {
|
|||
// @Success 200 {object} v1.vikunjaInfos
|
||||
// @Router /info [get]
|
||||
func Info(c echo.Context) error {
|
||||
infos := vikunjaInfos{
|
||||
info := vikunjaInfos{
|
||||
Version: version.Version,
|
||||
FrontendURL: config.ServiceFrontendurl.GetString(),
|
||||
Motd: config.ServiceMotd.GetString(),
|
||||
|
@ -50,9 +54,24 @@ func Info(c echo.Context) error {
|
|||
MaxFileSize: config.FilesMaxSize.GetString(),
|
||||
RegistrationEnabled: config.ServiceEnableRegistration.GetBool(),
|
||||
TaskAttachmentsEnabled: config.ServiceEnableTaskAttachments.GetBool(),
|
||||
TotpEnabled: config.ServiceEnableTotp.GetBool(),
|
||||
}
|
||||
|
||||
// Migrators
|
||||
if config.MigrationWunderlistEnable.GetBool() {
|
||||
infos.AvailableMigrators = append(infos.AvailableMigrators, "wunderlist")
|
||||
m := &wunderlist.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
return c.JSON(http.StatusOK, infos)
|
||||
if config.MigrationTodoistEnable.GetBool() {
|
||||
m := &todoist.Migration{}
|
||||
info.AvailableMigrators = append(info.AvailableMigrators, m.Name())
|
||||
}
|
||||
|
||||
if config.BackgroundsEnabled.GetBool() {
|
||||
if config.BackgroundsUnsplashEnabled.GetBool() {
|
||||
info.EnabledBackgroundProviders = append(info.EnabledBackgroundProviders, "unsplash")
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package v1
|
|||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
user2 "code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
|
@ -46,11 +45,12 @@ func UploadTaskAttachment(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Rights check
|
||||
user, err := user2.GetCurrentUser(c)
|
||||
auth, err := GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
can, err := taskAttachment.CanCreate(user)
|
||||
|
||||
can, err := taskAttachment.CanCreate(auth)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ func UploadTaskAttachment(c echo.Context) error {
|
|||
}
|
||||
defer f.Close()
|
||||
|
||||
err = ta.NewAttachment(f, file.Filename, uint64(file.Size), user)
|
||||
err = ta.NewAttachment(f, file.Filename, uint64(file.Size), auth)
|
||||
if err != nil {
|
||||
r.Errors = append(r.Errors, handler.HandleHTTPError(err, c))
|
||||
continue
|
||||
|
@ -115,11 +115,11 @@ func GetTaskAttachment(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Rights check
|
||||
user, err := user2.GetCurrentUser(c)
|
||||
auth, err := GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
can, err := taskAttachment.CanRead(user)
|
||||
can, err := taskAttachment.CanRead(auth)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ func RegisterUser(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Add its namespace
|
||||
newN := &models.Namespace{Name: newUser.Username, Description: newUser.Username + "'s namespace.", Owner: newUser}
|
||||
newN := &models.Namespace{Title: newUser.Username, Description: newUser.Username + "'s namespace.", Owner: newUser}
|
||||
err = newN.Create(newUser)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
|
|
|
@ -73,11 +73,12 @@ func ListUsersForList(c echo.Context) error {
|
|||
}
|
||||
|
||||
list := models.List{ID: listID}
|
||||
currentUser, err := user.GetCurrentUser(c)
|
||||
auth, err := GetAuthFromClaims(c)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
canRead, err := list.CanRead(currentUser)
|
||||
|
||||
canRead, err := list.CanRead(auth)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ func UserTOTPEnable(c echo.Context) error {
|
|||
if he, is := err.(*echo.HTTPError); is {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided."))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
|
||||
}
|
||||
|
||||
err = user.EnableTOTP(passcode)
|
||||
|
@ -113,7 +113,7 @@ func UserTOTPDisable(c echo.Context) error {
|
|||
if he, is := err.(*echo.HTTPError); is {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided."))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
|
||||
}
|
||||
|
||||
u, err := user.GetCurrentUser(c)
|
||||
|
|
|
@ -47,7 +47,7 @@ func UpdateUserEmail(c echo.Context) (err error) {
|
|||
if he, is := err.(*echo.HTTPError); is {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided."))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
|
||||
}
|
||||
|
||||
emailUpdate.User, err = user.GetCurrentUser(c)
|
||||
|
|
|
@ -137,7 +137,7 @@ func (vcls *VikunjaCaldavListStorage) GetResourcesByList(rpaths []string) ([]dat
|
|||
task: t,
|
||||
}
|
||||
r := data.NewResource(getTaskURL(t), &rr)
|
||||
r.Name = t.Text
|
||||
r.Name = t.Title
|
||||
resources = append(resources, r)
|
||||
}
|
||||
|
||||
|
@ -159,7 +159,7 @@ func (vcls *VikunjaCaldavListStorage) GetResourcesByFilters(rpath string, filter
|
|||
isCollection: false,
|
||||
}
|
||||
r := data.NewResource(getTaskURL(t), &rr)
|
||||
r.Name = t.Text
|
||||
r.Name = t.Title
|
||||
resources = append(resources, r)
|
||||
}
|
||||
return resources, nil
|
||||
|
|
|
@ -37,7 +37,7 @@ func getCaldavTodosForTasks(list *models.List) string {
|
|||
caldavtodos = append(caldavtodos, &caldav.Todo{
|
||||
Timestamp: t.Updated,
|
||||
UID: t.UID,
|
||||
Summary: t.Text,
|
||||
Summary: t.Title,
|
||||
Description: t.Description,
|
||||
Completed: t.DoneAt,
|
||||
// Organizer: &t.CreatedBy, // Disabled until we figure out how this works
|
||||
|
@ -91,7 +91,7 @@ func parseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||
|
||||
vTask = &models.Task{
|
||||
UID: task["UID"],
|
||||
Text: task["SUMMARY"],
|
||||
Title: task["SUMMARY"],
|
||||
Description: task["DESCRIPTION"],
|
||||
Priority: priority,
|
||||
DueDate: caldavTimeToTimestamp(task["DUE"]),
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
"code.vikunja.io/api/pkg/user"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setupMetrics(a *echo.Group) {
|
||||
|
@ -77,11 +76,6 @@ func setupMetrics(a *echo.Group) {
|
|||
}
|
||||
}
|
||||
|
||||
// init active users, sometimes we'll have garbage from previous runs in redis instead
|
||||
if err := metrics.SetActiveUsers([]*metrics.ActiveUser{}); err != nil {
|
||||
log.Fatalf("Could not set initial count for active users, error was %s", err)
|
||||
}
|
||||
|
||||
a.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
|
||||
}
|
||||
|
||||
|
@ -110,22 +104,5 @@ func updateActiveUsersFromContext(c echo.Context) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
allActiveUsers, err := metrics.GetActiveUsers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var uupdated bool
|
||||
for in, u := range allActiveUsers {
|
||||
if u.UserID == auth.GetID() {
|
||||
allActiveUsers[in].LastSeen = time.Now()
|
||||
uupdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if !uupdated {
|
||||
allActiveUsers = append(allActiveUsers, &metrics.ActiveUser{UserID: auth.GetID(), LastSeen: time.Now()})
|
||||
}
|
||||
|
||||
return metrics.SetActiveUsers(allActiveUsers)
|
||||
return metrics.SetUserActive(auth)
|
||||
}
|
||||
|
|
|
@ -47,8 +47,12 @@ import (
|
|||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/background"
|
||||
backgroundHandler "code.vikunja.io/api/pkg/modules/background/handler"
|
||||
"code.vikunja.io/api/pkg/modules/background/unsplash"
|
||||
"code.vikunja.io/api/pkg/modules/migration"
|
||||
migrationHandler "code.vikunja.io/api/pkg/modules/migration/handler"
|
||||
"code.vikunja.io/api/pkg/modules/migration/todoist"
|
||||
"code.vikunja.io/api/pkg/modules/migration/wunderlist"
|
||||
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"code.vikunja.io/api/pkg/routes/caldav"
|
||||
|
@ -209,11 +213,14 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
u.GET("s", apiv1.UserList)
|
||||
u.POST("/token", apiv1.RenewToken)
|
||||
u.POST("/settings/email", apiv1.UpdateUserEmail)
|
||||
u.GET("/settings/totp", apiv1.UserTOTP)
|
||||
u.POST("/settings/totp/enroll", apiv1.UserTOTPEnroll)
|
||||
u.POST("/settings/totp/enable", apiv1.UserTOTPEnable)
|
||||
u.POST("/settings/totp/disable", apiv1.UserTOTPDisable)
|
||||
u.GET("/settings/totp/qrcode", apiv1.UserTOTPQrCode)
|
||||
|
||||
if config.ServiceEnableTotp.GetBool() {
|
||||
u.GET("/settings/totp", apiv1.UserTOTP)
|
||||
u.POST("/settings/totp/enroll", apiv1.UserTOTPEnroll)
|
||||
u.POST("/settings/totp/enable", apiv1.UserTOTPEnable)
|
||||
u.POST("/settings/totp/disable", apiv1.UserTOTPDisable)
|
||||
u.GET("/settings/totp/qrcode", apiv1.UserTOTPQrCode)
|
||||
}
|
||||
|
||||
listHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
|
@ -433,6 +440,32 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
}
|
||||
wunderlistMigrationHandler.RegisterRoutes(m)
|
||||
}
|
||||
|
||||
// Todoist
|
||||
if config.MigrationTodoistEnable.GetBool() {
|
||||
todoistMigrationHandler := &migrationHandler.MigrationWeb{
|
||||
MigrationStruct: func() migration.Migrator {
|
||||
return &todoist.Migration{}
|
||||
},
|
||||
}
|
||||
todoistMigrationHandler.RegisterRoutes(m)
|
||||
}
|
||||
|
||||
// List Backgrounds
|
||||
if config.BackgroundsEnabled.GetBool() {
|
||||
a.GET("/lists/:list/background", backgroundHandler.GetListBackground)
|
||||
if config.BackgroundsUnsplashEnabled.GetBool() {
|
||||
unsplashBackgroundProvider := &backgroundHandler.BackgroundProvider{
|
||||
Provider: func() background.Provider {
|
||||
return &unsplash.Provider{}
|
||||
},
|
||||
}
|
||||
a.GET("/backgrounds/unsplash/search", unsplashBackgroundProvider.SearchBackgrounds)
|
||||
a.POST("/lists/:list/backgrounds/unsplash", unsplashBackgroundProvider.SetBackground)
|
||||
a.GET("/backgrounds/unsplash/images/:image/thumb", unsplash.ProxyUnsplashThumb)
|
||||
a.GET("/backgrounds/unsplash/images/:image", unsplash.ProxyUnsplashImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerCalDavRoutes(c *echo.Group) {
|
||||
|
|
|
@ -85,7 +85,7 @@ func IsErrNoUsernamePassword(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrNoUsernamePassword) Error() string {
|
||||
return fmt.Sprintf("No username and password provided")
|
||||
return "No username and password provided"
|
||||
}
|
||||
|
||||
// ErrCodeNoUsernamePassword holds the unique world-error code of this error
|
||||
|
@ -129,7 +129,7 @@ func IsErrCouldNotGetUserID(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrCouldNotGetUserID) Error() string {
|
||||
return fmt.Sprintf("Could not get user ID")
|
||||
return "Could not get user ID"
|
||||
}
|
||||
|
||||
// ErrCodeCouldNotGetUserID holds the unique world-error code of this error
|
||||
|
@ -208,7 +208,7 @@ type ErrWrongUsernameOrPassword struct {
|
|||
}
|
||||
|
||||
func (err ErrWrongUsernameOrPassword) Error() string {
|
||||
return fmt.Sprintf("Wrong username or password")
|
||||
return "Wrong username or password"
|
||||
}
|
||||
|
||||
// ErrCodeWrongUsernameOrPassword holds the unique world-error code of this error
|
||||
|
@ -258,7 +258,7 @@ func IsErrEmptyNewPassword(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrEmptyNewPassword) Error() string {
|
||||
return fmt.Sprintf("New password is empty")
|
||||
return "New password is empty"
|
||||
}
|
||||
|
||||
// ErrCodeEmptyNewPassword holds the unique world-error code of this error
|
||||
|
@ -279,7 +279,7 @@ func IsErrEmptyOldPassword(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrEmptyOldPassword) Error() string {
|
||||
return fmt.Sprintf("Old password is empty")
|
||||
return "Old password is empty"
|
||||
}
|
||||
|
||||
// ErrCodeEmptyOldPassword holds the unique world-error code of this error
|
||||
|
@ -300,7 +300,7 @@ func IsErrTOTPAlreadyEnabled(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrTOTPAlreadyEnabled) Error() string {
|
||||
return fmt.Sprintf("Totp is already enabled for this user")
|
||||
return "Totp is already enabled for this user"
|
||||
}
|
||||
|
||||
// ErrCodeTOTPAlreadyEnabled holds the unique world-error code of this error
|
||||
|
@ -325,7 +325,7 @@ func IsErrTOTPNotEnabled(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrTOTPNotEnabled) Error() string {
|
||||
return fmt.Sprintf("Totp is not enabled for this user")
|
||||
return "Totp is not enabled for this user"
|
||||
}
|
||||
|
||||
// ErrCodeTOTPNotEnabled holds the unique world-error code of this error
|
||||
|
@ -352,7 +352,7 @@ func IsErrInvalidTOTPPasscode(err error) bool {
|
|||
}
|
||||
|
||||
func (err ErrInvalidTOTPPasscode) Error() string {
|
||||
return fmt.Sprintf("Invalid totp passcode")
|
||||
return "Invalid totp passcode"
|
||||
}
|
||||
|
||||
// ErrCodeInvalidTOTPPasscode holds the unique world-error code of this error
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"image"
|
||||
|
@ -26,7 +27,7 @@ import (
|
|||
type TOTP struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"-"`
|
||||
UserID int64 `xorm:"int(11) not null" json:"-"`
|
||||
Secret string `xorm:"varchar(20) not null" json:"secret"`
|
||||
Secret string `xorm:"text not null" json:"secret"`
|
||||
// The totp entry will only be enabled after the user verified they have a working totp setup.
|
||||
Enabled bool `xorm:"null" json:"enabled"`
|
||||
// The totp url used to be able to enroll the user later
|
||||
|
@ -46,6 +47,9 @@ type TOTPPasscode struct {
|
|||
|
||||
// TOTPEnabledForUser checks if totp is enabled for a user - not if it is activated, use GetTOTPForUser to check that.
|
||||
func TOTPEnabledForUser(user *User) (bool, error) {
|
||||
if !config.ServiceEnableTotp.GetBool() {
|
||||
return false, nil
|
||||
}
|
||||
return x.Where("user_id = ?", user.ID).Exist(&TOTP{})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Set the uid and gid of the vikunja run user
|
||||
usermod --non-unique --uid ${PUID} vikunja
|
||||
groupmod --non-unique --gid ${PGID} vikunja
|
||||
|
||||
su vikunja -c '/app/vikunja/vikunja'
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1 @@
|
|||
This repository holds Go packages for accessing Security Support Provider Interface on Windows.
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package sspi
|
||||
|
||||
import (
|
||||
"io"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (b *SecBuffer) Set(buftype uint32, data []byte) {
|
||||
b.BufferType = buftype
|
||||
if len(data) > 0 {
|
||||
b.Buffer = &data[0]
|
||||
b.BufferSize = uint32(len(data))
|
||||
} else {
|
||||
b.Buffer = nil
|
||||
b.BufferSize = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SecBuffer) Free() error {
|
||||
if b.Buffer == nil {
|
||||
return nil
|
||||
}
|
||||
return FreeContextBuffer((*byte)(unsafe.Pointer(b.Buffer)))
|
||||
}
|
||||
|
||||
func (b *SecBuffer) Bytes() []byte {
|
||||
if b.Buffer == nil || b.BufferSize <= 0 {
|
||||
return nil
|
||||
}
|
||||
return (*[2 << 20]byte)(unsafe.Pointer(b.Buffer))[:b.BufferSize]
|
||||
}
|
||||
|
||||
func (b *SecBuffer) WriteAll(w io.Writer) (int, error) {
|
||||
if b.BufferSize == 0 || b.Buffer == nil {
|
||||
return 0, nil
|
||||
}
|
||||
data := b.Bytes()
|
||||
total := 0
|
||||
for {
|
||||
n, err := w.Write(data)
|
||||
total += n
|
||||
if err != nil {
|
||||
return total, err
|
||||
}
|
||||
if n >= len(data) {
|
||||
break
|
||||
}
|
||||
data = data[n:]
|
||||
}
|
||||
return total, nil
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sspi
|
||||
|
||||
//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -systemdll=false -output=zsyscall_windows.go syscall.go
|
|
@ -0,0 +1,462 @@
|
|||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
// Package negotiate provides access to the Microsoft Negotiate SSP Package.
|
||||
//
|
||||
package negotiate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/alexbrainman/sspi"
|
||||
)
|
||||
|
||||
// TODO: maybe (if possible) move all winapi related out of sspi and into sspi/internal/winapi
|
||||
|
||||
// PackageInfo contains Negotiate SSP package description.
|
||||
var PackageInfo *sspi.PackageInfo
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
PackageInfo, err = sspi.QueryPackageInfo(sspi.NEGOSSP_NAME)
|
||||
if err != nil {
|
||||
panic("failed to fetch Negotiate package info: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func acquireCredentials(principalName string, creduse uint32, ai *sspi.SEC_WINNT_AUTH_IDENTITY) (*sspi.Credentials, error) {
|
||||
c, err := sspi.AcquireCredentials(principalName, sspi.NEGOSSP_NAME, creduse, (*byte)(unsafe.Pointer(ai)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// AcquireCurrentUserCredentials acquires credentials of currently
|
||||
// logged on user. These will be used by the client to authenticate
|
||||
// itself to the server. It will also be used by the server
|
||||
// to impersonate the user.
|
||||
func AcquireCurrentUserCredentials() (*sspi.Credentials, error) {
|
||||
return acquireCredentials("", sspi.SECPKG_CRED_OUTBOUND, nil)
|
||||
}
|
||||
|
||||
// TODO: see if I can share this common ntlm and negotiate code
|
||||
|
||||
// AcquireUserCredentials acquires credentials of user described by
|
||||
// domain, username and password. These will be used by the client to
|
||||
// authenticate itself to the server. It will also be used by the
|
||||
// server to impersonate the user.
|
||||
func AcquireUserCredentials(domain, username, password string) (*sspi.Credentials, error) {
|
||||
if len(username) == 0 {
|
||||
return nil, errors.New("username parameter cannot be empty")
|
||||
}
|
||||
d, err := syscall.UTF16FromString(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
u, err := syscall.UTF16FromString(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := syscall.UTF16FromString(password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ai := sspi.SEC_WINNT_AUTH_IDENTITY{
|
||||
User: &u[0],
|
||||
UserLength: uint32(len(u) - 1), // do not count terminating 0
|
||||
Domain: &d[0],
|
||||
DomainLength: uint32(len(d) - 1), // do not count terminating 0
|
||||
Password: &p[0],
|
||||
PasswordLength: uint32(len(p) - 1), // do not count terminating 0
|
||||
Flags: sspi.SEC_WINNT_AUTH_IDENTITY_UNICODE,
|
||||
}
|
||||
return acquireCredentials("", sspi.SECPKG_CRED_OUTBOUND, &ai)
|
||||
}
|
||||
|
||||
// AcquireServerCredentials acquires server credentials that will
|
||||
// be used to authenticate clients.
|
||||
// The principalName parameter is passed to the underlying call to
|
||||
// the winapi AcquireCredentialsHandle function (and specifies the
|
||||
// name of the principal whose credentials the underlying handle
|
||||
// will reference).
|
||||
// As a special case, using an empty string for the principal name
|
||||
// will require the credential of the user under whose security context
|
||||
// the current process is running.
|
||||
func AcquireServerCredentials(principalName string) (*sspi.Credentials, error) {
|
||||
return acquireCredentials(principalName, sspi.SECPKG_CRED_INBOUND, nil)
|
||||
}
|
||||
|
||||
func updateContext(c *sspi.Context, dst, src []byte, targetName *uint16) (authCompleted bool, n int, err error) {
|
||||
var inBuf, outBuf [1]sspi.SecBuffer
|
||||
inBuf[0].Set(sspi.SECBUFFER_TOKEN, src)
|
||||
inBufs := &sspi.SecBufferDesc{
|
||||
Version: sspi.SECBUFFER_VERSION,
|
||||
BuffersCount: 1,
|
||||
Buffers: &inBuf[0],
|
||||
}
|
||||
outBuf[0].Set(sspi.SECBUFFER_TOKEN, dst)
|
||||
outBufs := &sspi.SecBufferDesc{
|
||||
Version: sspi.SECBUFFER_VERSION,
|
||||
BuffersCount: 1,
|
||||
Buffers: &outBuf[0],
|
||||
}
|
||||
ret := c.Update(targetName, outBufs, inBufs)
|
||||
switch ret {
|
||||
case sspi.SEC_E_OK:
|
||||
// session established -> return success
|
||||
return true, int(outBuf[0].BufferSize), nil
|
||||
case sspi.SEC_I_COMPLETE_NEEDED, sspi.SEC_I_COMPLETE_AND_CONTINUE:
|
||||
ret = sspi.CompleteAuthToken(c.Handle, outBufs)
|
||||
if ret != sspi.SEC_E_OK {
|
||||
return false, 0, ret
|
||||
}
|
||||
case sspi.SEC_I_CONTINUE_NEEDED:
|
||||
default:
|
||||
return false, 0, ret
|
||||
}
|
||||
return false, int(outBuf[0].BufferSize), nil
|
||||
}
|
||||
|
||||
func makeSignature(c *sspi.Context, msg []byte, qop, seqno uint32) ([]byte, error) {
|
||||
_, maxSignature, _, _, err := c.Sizes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if maxSignature == 0 {
|
||||
return nil, errors.New("integrity services are not requested or unavailable")
|
||||
}
|
||||
|
||||
var b [2]sspi.SecBuffer
|
||||
b[0].Set(sspi.SECBUFFER_DATA, msg)
|
||||
b[1].Set(sspi.SECBUFFER_TOKEN, make([]byte, maxSignature))
|
||||
|
||||
ret := sspi.MakeSignature(c.Handle, qop, sspi.NewSecBufferDesc(b[:]), seqno)
|
||||
if ret != sspi.SEC_E_OK {
|
||||
return nil, ret
|
||||
}
|
||||
|
||||
return b[1].Bytes(), nil
|
||||
}
|
||||
|
||||
func encryptMessage(c *sspi.Context, msg []byte, qop, seqno uint32) ([]byte, error) {
|
||||
_ /*maxToken*/, maxSignature, cBlockSize, cSecurityTrailer, err := c.Sizes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if maxSignature == 0 {
|
||||
return nil, errors.New("integrity services are not requested or unavailable")
|
||||
}
|
||||
|
||||
var b [3]sspi.SecBuffer
|
||||
b[0].Set(sspi.SECBUFFER_TOKEN, make([]byte, cSecurityTrailer))
|
||||
b[1].Set(sspi.SECBUFFER_DATA, msg)
|
||||
b[2].Set(sspi.SECBUFFER_PADDING, make([]byte, cBlockSize))
|
||||
|
||||
ret := sspi.EncryptMessage(c.Handle, qop, sspi.NewSecBufferDesc(b[:]), seqno)
|
||||
if ret != sspi.SEC_E_OK {
|
||||
return nil, ret
|
||||
}
|
||||
|
||||
r0, r1, r2 := b[0].Bytes(), b[1].Bytes(), b[2].Bytes()
|
||||
res := make([]byte, 0, len(r0)+len(r1)+len(r2))
|
||||
res = append(res, r0...)
|
||||
res = append(res, r1...)
|
||||
res = append(res, r2...)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func decryptMessage(c *sspi.Context, msg []byte, seqno uint32) (uint32, []byte, error) {
|
||||
var b [2]sspi.SecBuffer
|
||||
b[0].Set(sspi.SECBUFFER_STREAM, msg)
|
||||
b[1].Set(sspi.SECBUFFER_DATA, []byte{})
|
||||
|
||||
var qop uint32
|
||||
ret := sspi.DecryptMessage(c.Handle, sspi.NewSecBufferDesc(b[:]), seqno, &qop)
|
||||
if ret != sspi.SEC_E_OK {
|
||||
return qop, nil, ret
|
||||
}
|
||||
|
||||
return qop, b[1].Bytes(), nil
|
||||
}
|
||||
|
||||
func verifySignature(c *sspi.Context, msg, token []byte, seqno uint32) (uint32, error) {
|
||||
var b [2]sspi.SecBuffer
|
||||
b[0].Set(sspi.SECBUFFER_DATA, msg)
|
||||
b[1].Set(sspi.SECBUFFER_TOKEN, token)
|
||||
|
||||
var qop uint32
|
||||
|
||||
ret := sspi.VerifySignature(c.Handle, sspi.NewSecBufferDesc(b[:]), seqno, &qop)
|
||||
if ret != sspi.SEC_E_OK {
|
||||
return 0, ret
|
||||
}
|
||||
|
||||
return qop, nil
|
||||
}
|
||||
|
||||
// ClientContext is used by the client to manage all steps of Negotiate negotiation.
|
||||
type ClientContext struct {
|
||||
sctxt *sspi.Context
|
||||
targetName *uint16
|
||||
}
|
||||
|
||||
// NewClientContext creates a new client context. It uses client
|
||||
// credentials cred generated by AcquireCurrentUserCredentials or
|
||||
// AcquireUserCredentials and SPN to start a client Negotiate
|
||||
// negotiation sequence. targetName is the service principal name
|
||||
// (SPN) or the security context of the destination server.
|
||||
// NewClientContext returns a new token to be sent to the server.
|
||||
func NewClientContext(cred *sspi.Credentials, targetName string) (cc *ClientContext, outputToken []byte, err error) {
|
||||
return NewClientContextWithFlags(cred, targetName, sspi.ISC_REQ_CONNECTION)
|
||||
}
|
||||
|
||||
// NewClientContextWithFlags creates a new client context. It uses client
|
||||
// credentials cred generated by AcquireCurrentUserCredentials or
|
||||
// AcquireUserCredentials and SPN to start a client Negotiate
|
||||
// negotiation sequence. targetName is the service principal name
|
||||
// (SPN) or the security context of the destination server.
|
||||
// The flags parameter is used to indicate requests for the context
|
||||
// (for example sspi.ISC_REQ_CONFIDENTIALITY|sspi.ISC_REQ_REPLAY_DETECT)
|
||||
// NewClientContextWithFlags returns a new token to be sent to the server.
|
||||
func NewClientContextWithFlags(cred *sspi.Credentials, targetName string, flags uint32) (cc *ClientContext, outputToken []byte, err error) {
|
||||
var tname *uint16
|
||||
if len(targetName) > 0 {
|
||||
p, err2 := syscall.UTF16FromString(targetName)
|
||||
if err2 != nil {
|
||||
return nil, nil, err2
|
||||
}
|
||||
if len(p) > 0 {
|
||||
tname = &p[0]
|
||||
}
|
||||
}
|
||||
otoken := make([]byte, PackageInfo.MaxToken)
|
||||
c := sspi.NewClientContext(cred, flags)
|
||||
|
||||
authCompleted, n, err2 := updateContext(c, otoken, nil, tname)
|
||||
if err2 != nil {
|
||||
return nil, nil, err2
|
||||
}
|
||||
if authCompleted {
|
||||
c.Release()
|
||||
return nil, nil, errors.New("negotiate authentication should not be completed yet")
|
||||
}
|
||||
if n == 0 {
|
||||
c.Release()
|
||||
return nil, nil, errors.New("negotiate token should not be empty")
|
||||
}
|
||||
otoken = otoken[:n]
|
||||
return &ClientContext{sctxt: c, targetName: tname}, otoken, nil
|
||||
}
|
||||
|
||||
// Release free up resources associated with client context c.
|
||||
func (c *ClientContext) Release() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.sctxt.Release()
|
||||
}
|
||||
|
||||
// Expiry returns c expiry time.
|
||||
func (c *ClientContext) Expiry() time.Time {
|
||||
return c.sctxt.Expiry()
|
||||
}
|
||||
|
||||
// Update advances client part of Negotiate negotiation c. It uses
|
||||
// token received from the server and returns true if client part
|
||||
// of authentication is complete. It also returns new token to be
|
||||
// sent to the server.
|
||||
func (c *ClientContext) Update(token []byte) (authCompleted bool, outputToken []byte, err error) {
|
||||
otoken := make([]byte, PackageInfo.MaxToken)
|
||||
authDone, n, err2 := updateContext(c.sctxt, otoken, token, c.targetName)
|
||||
if err2 != nil {
|
||||
return false, nil, err2
|
||||
}
|
||||
if n == 0 && !authDone {
|
||||
return false, nil, errors.New("negotiate token should not be empty")
|
||||
}
|
||||
otoken = otoken[:n]
|
||||
return authDone, otoken, nil
|
||||
}
|
||||
|
||||
// Sizes queries the client context for the sizes used in per-message
|
||||
// functions. It returns the maximum token size used in authentication
|
||||
// exchanges, the maximum signature size, the preferred integral size of
|
||||
// messages, the size of any security trailer, and any error.
|
||||
func (c *ClientContext) Sizes() (uint32, uint32, uint32, uint32, error) {
|
||||
return c.sctxt.Sizes()
|
||||
}
|
||||
|
||||
// MakeSignature uses the established client context to create a signature
|
||||
// for the given message using the provided quality of protection flags and
|
||||
// sequence number. It returns the signature token in addition to any error.
|
||||
func (c *ClientContext) MakeSignature(msg []byte, qop, seqno uint32) ([]byte, error) {
|
||||
return makeSignature(c.sctxt, msg, qop, seqno)
|
||||
}
|
||||
|
||||
// VerifySignature uses the established client context and signature token
|
||||
// to check that the provided message hasn't been tampered or received out
|
||||
// of sequence. It returns any quality of protection flags and any error
|
||||
// that occurred.
|
||||
func (c *ClientContext) VerifySignature(msg, token []byte, seqno uint32) (uint32, error) {
|
||||
return verifySignature(c.sctxt, msg, token, seqno)
|
||||
}
|
||||
|
||||
// EncryptMessage uses the established client context to encrypt a message
|
||||
// using the provided quality of protection flags and sequence number.
|
||||
// It returns the signature token in addition to any error.
|
||||
// IMPORTANT: the input msg parameter is updated in place by the low-level windows api
|
||||
// so must be copied if the initial content should not be modified.
|
||||
func (c *ClientContext) EncryptMessage(msg []byte, qop, seqno uint32) ([]byte, error) {
|
||||
return encryptMessage(c.sctxt, msg, qop, seqno)
|
||||
}
|
||||
|
||||
// DecryptMessage uses the established client context to decrypt a message
|
||||
// using the provided sequence number.
|
||||
// It returns the quality of protection flag and the decrypted message in addition to any error.
|
||||
func (c *ClientContext) DecryptMessage(msg []byte, seqno uint32) (uint32, []byte, error) {
|
||||
return decryptMessage(c.sctxt, msg, seqno)
|
||||
}
|
||||
|
||||
// VerifyFlags determines if all flags used to construct the client context
|
||||
// were honored (see NewClientContextWithFlags). It should be called after c.Update.
|
||||
func (c *ClientContext) VerifyFlags() error {
|
||||
return c.sctxt.VerifyFlags()
|
||||
}
|
||||
|
||||
// VerifySelectiveFlags determines if the given flags were honored (see NewClientContextWithFlags).
|
||||
// It should be called after c.Update.
|
||||
func (c *ClientContext) VerifySelectiveFlags(flags uint32) error {
|
||||
return c.sctxt.VerifySelectiveFlags(flags)
|
||||
}
|
||||
|
||||
// ServerContext is used by the server to manage all steps of Negotiate
|
||||
// negotiation. Once authentication is completed the context can be
|
||||
// used to impersonate client.
|
||||
type ServerContext struct {
|
||||
sctxt *sspi.Context
|
||||
}
|
||||
|
||||
// NewServerContext creates new server context. It uses server
|
||||
// credentials created by AcquireServerCredentials and token from
|
||||
// the client to start server Negotiate negotiation sequence.
|
||||
// It also returns new token to be sent to the client.
|
||||
func NewServerContext(cred *sspi.Credentials, token []byte) (sc *ServerContext, authDone bool, outputToken []byte, err error) {
|
||||
otoken := make([]byte, PackageInfo.MaxToken)
|
||||
c := sspi.NewServerContext(cred, sspi.ASC_REQ_CONNECTION)
|
||||
authDone, n, err2 := updateContext(c, otoken, token, nil)
|
||||
if err2 != nil {
|
||||
return nil, false, nil, err2
|
||||
}
|
||||
otoken = otoken[:n]
|
||||
return &ServerContext{sctxt: c}, authDone, otoken, nil
|
||||
}
|
||||
|
||||
// Release free up resources associated with server context c.
|
||||
func (c *ServerContext) Release() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return c.sctxt.Release()
|
||||
}
|
||||
|
||||
// Expiry returns c expiry time.
|
||||
func (c *ServerContext) Expiry() time.Time {
|
||||
return c.sctxt.Expiry()
|
||||
}
|
||||
|
||||
// Update advances server part of Negotiate negotiation c. It uses
|
||||
// token received from the client and returns true if server part
|
||||
// of authentication is complete. It also returns new token to be
|
||||
// sent to the client.
|
||||
func (c *ServerContext) Update(token []byte) (authCompleted bool, outputToken []byte, err error) {
|
||||
otoken := make([]byte, PackageInfo.MaxToken)
|
||||
authDone, n, err2 := updateContext(c.sctxt, otoken, token, nil)
|
||||
if err2 != nil {
|
||||
return false, nil, err2
|
||||
}
|
||||
if n == 0 && !authDone {
|
||||
return false, nil, errors.New("negotiate token should not be empty")
|
||||
}
|
||||
otoken = otoken[:n]
|
||||
return authDone, otoken, nil
|
||||
}
|
||||
|
||||
const _SECPKG_ATTR_NATIVE_NAMES = 13
|
||||
|
||||
type _SecPkgContext_NativeNames struct {
|
||||
ClientName *uint16
|
||||
ServerName *uint16
|
||||
}
|
||||
|
||||
// GetUsername returns the username corresponding to the authenticated client
|
||||
func (c *ServerContext) GetUsername() (string, error) {
|
||||
var ns _SecPkgContext_NativeNames
|
||||
ret := sspi.QueryContextAttributes(c.sctxt.Handle, _SECPKG_ATTR_NATIVE_NAMES, (*byte)(unsafe.Pointer(&ns)))
|
||||
if ret != sspi.SEC_E_OK {
|
||||
return "", ret
|
||||
}
|
||||
sspi.FreeContextBuffer((*byte)(unsafe.Pointer(ns.ServerName)))
|
||||
defer sspi.FreeContextBuffer((*byte)(unsafe.Pointer(ns.ClientName)))
|
||||
return syscall.UTF16ToString((*[2 << 20]uint16)(unsafe.Pointer(ns.ClientName))[:]), nil
|
||||
}
|
||||
|
||||
// ImpersonateUser changes current OS thread user. New user is
|
||||
// the user as specified by client credentials.
|
||||
func (c *ServerContext) ImpersonateUser() error {
|
||||
return c.sctxt.ImpersonateUser()
|
||||
}
|
||||
|
||||
// RevertToSelf stops impersonation. It changes current OS thread
|
||||
// user to what it was before ImpersonateUser was executed.
|
||||
func (c *ServerContext) RevertToSelf() error {
|
||||
return c.sctxt.RevertToSelf()
|
||||
}
|
||||
|
||||
// Sizes queries the server context for the sizes used in per-message
|
||||
// functions. It returns the maximum token size used in authentication
|
||||
// exchanges, the maximum signature size, the preferred integral size of
|
||||
// messages, the size of any security trailer, and any error.
|
||||
func (c *ServerContext) Sizes() (uint32, uint32, uint32, uint32, error) {
|
||||
return c.sctxt.Sizes()
|
||||
}
|
||||
|
||||
// MakeSignature uses the established server context to create a signature
|
||||
// for the given message using the provided quality of protection flags and
|
||||
// sequence number. It returns the signature token in addition to any error.
|
||||
func (c *ServerContext) MakeSignature(msg []byte, qop, seqno uint32) ([]byte, error) {
|
||||
return makeSignature(c.sctxt, msg, qop, seqno)
|
||||
}
|
||||
|
||||
// VerifySignature uses the established server context and signature token
|
||||
// to check that the provided message hasn't been tampered or received out
|
||||
// of sequence. It returns any quality of protection flags and any error
|
||||
// that occurred.
|
||||
func (c *ServerContext) VerifySignature(msg, token []byte, seqno uint32) (uint32, error) {
|
||||
return verifySignature(c.sctxt, msg, token, seqno)
|
||||
}
|
||||
|
||||
// EncryptMessage uses the established server context to encrypt a message
|
||||
// using the provided quality of protection flags and sequence number.
|
||||
// It returns the signature token in addition to any error.
|
||||
// IMPORTANT: the input msg parameter is updated in place by the low-level windows api
|
||||
// so must be copied if the initial content should not be modified.
|
||||
func (c *ServerContext) EncryptMessage(msg []byte, qop, seqno uint32) ([]byte, error) {
|
||||
return encryptMessage(c.sctxt, msg, qop, seqno)
|
||||
}
|
||||
|
||||
// DecryptMessage uses the established server context to decrypt a message
|
||||
// using the provided sequence number.
|
||||
// It returns the quality of protection flag and the decrypted message in addition to any error.
|
||||
func (c *ServerContext) DecryptMessage(msg []byte, seqno uint32) (uint32, []byte, error) {
|
||||
return decryptMessage(c.sctxt, msg, seqno)
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package sspi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// TODO: add documentation
|
||||
|
||||
type PackageInfo struct {
|
||||
Capabilities uint32
|
||||
Version uint16
|
||||
RPCID uint16
|
||||
MaxToken uint32
|
||||
Name string
|
||||
Comment string
|
||||
}
|
||||
|
||||
func QueryPackageInfo(pkgname string) (*PackageInfo, error) {
|
||||
name, err := syscall.UTF16PtrFromString(pkgname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pi *SecPkgInfo
|
||||
ret := QuerySecurityPackageInfo(name, &pi)
|
||||
if ret != SEC_E_OK {
|
||||
return nil, ret
|
||||
}
|
||||
defer FreeContextBuffer((*byte)(unsafe.Pointer(pi)))
|
||||
|
||||
return &PackageInfo{
|
||||
Capabilities: pi.Capabilities,
|
||||
Version: pi.Version,
|
||||
RPCID: pi.RPCID,
|
||||
MaxToken: pi.MaxToken,
|
||||
Name: syscall.UTF16ToString((*[2 << 12]uint16)(unsafe.Pointer(pi.Name))[:]),
|
||||
Comment: syscall.UTF16ToString((*[2 << 12]uint16)(unsafe.Pointer(pi.Comment))[:]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Credentials struct {
|
||||
Handle CredHandle
|
||||
expiry syscall.Filetime
|
||||
}
|
||||
|
||||
// AcquireCredentials calls the windows AcquireCredentialsHandle function and
|
||||
// returns Credentials containing a security handle that can be used for
|
||||
// InitializeSecurityContext or AcceptSecurityContext operations.
|
||||
// As a special case, passing an empty string as the principal parameter will
|
||||
// pass a null string to the underlying function.
|
||||
func AcquireCredentials(principal string, pkgname string, creduse uint32, authdata *byte) (*Credentials, error) {
|
||||
var principalName *uint16
|
||||
if principal != "" {
|
||||
var err error
|
||||
principalName, err = syscall.UTF16PtrFromString(principal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
name, err := syscall.UTF16PtrFromString(pkgname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var c Credentials
|
||||
ret := AcquireCredentialsHandle(principalName, name, creduse, nil, authdata, 0, 0, &c.Handle, &c.expiry)
|
||||
if ret != SEC_E_OK {
|
||||
return nil, ret
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (c *Credentials) Release() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
ret := FreeCredentialsHandle(&c.Handle)
|
||||
if ret != SEC_E_OK {
|
||||
return ret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Credentials) Expiry() time.Time {
|
||||
return time.Unix(0, c.expiry.Nanoseconds())
|
||||
}
|
||||
|
||||
// TODO: add functions to display and manage RequestedFlags and EstablishedFlags fields.
|
||||
// TODO: maybe get rid of RequestedFlags and EstablishedFlags fields, and replace them with input parameter for New...Context and return value of Update (instead of current bool parameter).
|
||||
|
||||
type updateFunc func(c *Context, targname *uint16, h, newh *CtxtHandle, out, in *SecBufferDesc) syscall.Errno
|
||||
|
||||
type Context struct {
|
||||
Cred *Credentials
|
||||
Handle *CtxtHandle
|
||||
handle CtxtHandle
|
||||
updFn updateFunc
|
||||
expiry syscall.Filetime
|
||||
RequestedFlags uint32
|
||||
EstablishedFlags uint32
|
||||
}
|
||||
|
||||
func NewClientContext(cred *Credentials, flags uint32) *Context {
|
||||
return &Context{
|
||||
Cred: cred,
|
||||
updFn: initialize,
|
||||
RequestedFlags: flags,
|
||||
}
|
||||
}
|
||||
|
||||
func NewServerContext(cred *Credentials, flags uint32) *Context {
|
||||
return &Context{
|
||||
Cred: cred,
|
||||
updFn: accept,
|
||||
RequestedFlags: flags,
|
||||
}
|
||||
}
|
||||
|
||||
func initialize(c *Context, targname *uint16, h, newh *CtxtHandle, out, in *SecBufferDesc) syscall.Errno {
|
||||
return InitializeSecurityContext(&c.Cred.Handle, h, targname, c.RequestedFlags,
|
||||
0, SECURITY_NATIVE_DREP, in, 0, newh, out, &c.EstablishedFlags, &c.expiry)
|
||||
}
|
||||
|
||||
func accept(c *Context, targname *uint16, h, newh *CtxtHandle, out, in *SecBufferDesc) syscall.Errno {
|
||||
return AcceptSecurityContext(&c.Cred.Handle, h, in, c.RequestedFlags,
|
||||
SECURITY_NATIVE_DREP, newh, out, &c.EstablishedFlags, &c.expiry)
|
||||
}
|
||||
|
||||
func (c *Context) Update(targname *uint16, out, in *SecBufferDesc) syscall.Errno {
|
||||
h := c.Handle
|
||||
if c.Handle == nil {
|
||||
c.Handle = &c.handle
|
||||
}
|
||||
return c.updFn(c, targname, h, c.Handle, out, in)
|
||||
}
|
||||
|
||||
func (c *Context) Release() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
ret := DeleteSecurityContext(c.Handle)
|
||||
if ret != SEC_E_OK {
|
||||
return ret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) Expiry() time.Time {
|
||||
return time.Unix(0, c.expiry.Nanoseconds())
|
||||
}
|
||||
|
||||
// TODO: add comment to function doco that this "impersonation" is applied to current OS thread.
|
||||
func (c *Context) ImpersonateUser() error {
|
||||
ret := ImpersonateSecurityContext(c.Handle)
|
||||
if ret != SEC_E_OK {
|
||||
return ret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) RevertToSelf() error {
|
||||
ret := RevertSecurityContext(c.Handle)
|
||||
if ret != SEC_E_OK {
|
||||
return ret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sizes queries the context for the sizes used in per-message functions.
|
||||
// It returns the maximum token size used in authentication exchanges, the
|
||||
// maximum signature size, the preferred integral size of messages, the
|
||||
// size of any security trailer, and any error.
|
||||
func (c *Context) Sizes() (uint32, uint32, uint32, uint32, error) {
|
||||
var s _SecPkgContext_Sizes
|
||||
ret := QueryContextAttributes(c.Handle, _SECPKG_ATTR_SIZES, (*byte)(unsafe.Pointer(&s)))
|
||||
if ret != SEC_E_OK {
|
||||
return 0, 0, 0, 0, ret
|
||||
}
|
||||
return s.MaxToken, s.MaxSignature, s.BlockSize, s.SecurityTrailer, nil
|
||||
}
|
||||
|
||||
// VerifyFlags determines if all flags used to construct the context
|
||||
// were honored (see NewClientContext). It should be called after c.Update.
|
||||
func (c *Context) VerifyFlags() error {
|
||||
return c.VerifySelectiveFlags(c.RequestedFlags)
|
||||
}
|
||||
|
||||
// VerifySelectiveFlags determines if the given flags were honored (see NewClientContext).
|
||||
// It should be called after c.Update.
|
||||
func (c *Context) VerifySelectiveFlags(flags uint32) error {
|
||||
if valid, missing, extra := verifySelectiveFlags(flags, c.RequestedFlags); !valid {
|
||||
return fmt.Errorf("sspi: invalid flags check: desired=%b requested=%b missing=%b extra=%b", flags, c.RequestedFlags, missing, extra)
|
||||
}
|
||||
if valid, missing, extra := verifySelectiveFlags(flags, c.EstablishedFlags); !valid {
|
||||
return fmt.Errorf("sspi: invalid flags: desired=%b established=%b missing=%b extra=%b", flags, c.EstablishedFlags, missing, extra)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifySelectiveFlags determines if all bits requested in flags are set in establishedFlags.
|
||||
// missing represents the bits set in flags that are not set in establishedFlags.
|
||||
// extra represents the bits set in establishedFlags that are not set in flags.
|
||||
// valid is true and missing is zero when establishedFlags has all of the requested flags.
|
||||
func verifySelectiveFlags(flags, establishedFlags uint32) (valid bool, missing, extra uint32) {
|
||||
missing = flags&establishedFlags ^ flags
|
||||
extra = flags | establishedFlags ^ flags
|
||||
valid = missing == 0
|
||||
return valid, missing, extra
|
||||
}
|
||||
|
||||
// NewSecBufferDesc returns an initialized SecBufferDesc describing the
|
||||
// provided SecBuffer.
|
||||
func NewSecBufferDesc(b []SecBuffer) *SecBufferDesc {
|
||||
return &SecBufferDesc{
|
||||
Version: SECBUFFER_VERSION,
|
||||
BuffersCount: uint32(len(b)),
|
||||
Buffers: &b[0],
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package sspi
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
SEC_E_OK = syscall.Errno(0)
|
||||
|
||||
SEC_I_COMPLETE_AND_CONTINUE = syscall.Errno(590612)
|
||||
SEC_I_COMPLETE_NEEDED = syscall.Errno(590611)
|
||||
SEC_I_CONTINUE_NEEDED = syscall.Errno(590610)
|
||||
|
||||
SEC_E_LOGON_DENIED = syscall.Errno(0x8009030c)
|
||||
SEC_E_CONTEXT_EXPIRED = syscall.Errno(0x80090317) // not sure if the value is valid
|
||||
SEC_E_INCOMPLETE_MESSAGE = syscall.Errno(0x80090318)
|
||||
|
||||
NTLMSP_NAME = "NTLM"
|
||||
MICROSOFT_KERBEROS_NAME = "Kerberos"
|
||||
NEGOSSP_NAME = "Negotiate"
|
||||
UNISP_NAME = "Microsoft Unified Security Protocol Provider"
|
||||
|
||||
_SECPKG_ATTR_SIZES = 0
|
||||
_SECPKG_ATTR_NAMES = 1
|
||||
_SECPKG_ATTR_LIFESPAN = 2
|
||||
_SECPKG_ATTR_DCE_INFO = 3
|
||||
_SECPKG_ATTR_STREAM_SIZES = 4
|
||||
_SECPKG_ATTR_KEY_INFO = 5
|
||||
_SECPKG_ATTR_AUTHORITY = 6
|
||||
_SECPKG_ATTR_PROTO_INFO = 7
|
||||
_SECPKG_ATTR_PASSWORD_EXPIRY = 8
|
||||
_SECPKG_ATTR_SESSION_KEY = 9
|
||||
_SECPKG_ATTR_PACKAGE_INFO = 10
|
||||
_SECPKG_ATTR_USER_FLAGS = 11
|
||||
_SECPKG_ATTR_NEGOTIATION_INFO = 12
|
||||
_SECPKG_ATTR_NATIVE_NAMES = 13
|
||||
_SECPKG_ATTR_FLAGS = 14
|
||||
)
|
||||
|
||||
type SecPkgInfo struct {
|
||||
Capabilities uint32
|
||||
Version uint16
|
||||
RPCID uint16
|
||||
MaxToken uint32
|
||||
Name *uint16
|
||||
Comment *uint16
|
||||
}
|
||||
|
||||
type _SecPkgContext_Sizes struct {
|
||||
MaxToken uint32
|
||||
MaxSignature uint32
|
||||
BlockSize uint32
|
||||
SecurityTrailer uint32
|
||||
}
|
||||
|
||||
//sys QuerySecurityPackageInfo(pkgname *uint16, pkginfo **SecPkgInfo) (ret syscall.Errno) = secur32.QuerySecurityPackageInfoW
|
||||
//sys FreeContextBuffer(buf *byte) (ret syscall.Errno) = secur32.FreeContextBuffer
|
||||
|
||||
const (
|
||||
SECPKG_CRED_INBOUND = 1
|
||||
SECPKG_CRED_OUTBOUND = 2
|
||||
SECPKG_CRED_BOTH = (SECPKG_CRED_OUTBOUND | SECPKG_CRED_INBOUND)
|
||||
|
||||
SEC_WINNT_AUTH_IDENTITY_UNICODE = 0x2
|
||||
)
|
||||
|
||||
type SEC_WINNT_AUTH_IDENTITY struct {
|
||||
User *uint16
|
||||
UserLength uint32
|
||||
Domain *uint16
|
||||
DomainLength uint32
|
||||
Password *uint16
|
||||
PasswordLength uint32
|
||||
Flags uint32
|
||||
}
|
||||
|
||||
type LUID struct {
|
||||
LowPart uint32
|
||||
HighPart int32
|
||||
}
|
||||
|
||||
type CredHandle struct {
|
||||
Lower uintptr
|
||||
Upper uintptr
|
||||
}
|
||||
|
||||
//sys AcquireCredentialsHandle(principal *uint16, pkgname *uint16, creduse uint32, logonid *LUID, authdata *byte, getkeyfn uintptr, getkeyarg uintptr, handle *CredHandle, expiry *syscall.Filetime) (ret syscall.Errno) = secur32.AcquireCredentialsHandleW
|
||||
//sys FreeCredentialsHandle(handle *CredHandle) (ret syscall.Errno) = secur32.FreeCredentialsHandle
|
||||
|
||||
const (
|
||||
SECURITY_NATIVE_DREP = 16
|
||||
|
||||
SECBUFFER_DATA = 1
|
||||
SECBUFFER_TOKEN = 2
|
||||
SECBUFFER_PKG_PARAMS = 3
|
||||
SECBUFFER_MISSING = 4
|
||||
SECBUFFER_EXTRA = 5
|
||||
SECBUFFER_STREAM_TRAILER = 6
|
||||
SECBUFFER_STREAM_HEADER = 7
|
||||
SECBUFFER_PADDING = 9
|
||||
SECBUFFER_STREAM = 10
|
||||
SECBUFFER_READONLY = 0x80000000
|
||||
SECBUFFER_ATTRMASK = 0xf0000000
|
||||
SECBUFFER_VERSION = 0
|
||||
SECBUFFER_EMPTY = 0
|
||||
|
||||
ISC_REQ_DELEGATE = 1
|
||||
ISC_REQ_MUTUAL_AUTH = 2
|
||||
ISC_REQ_REPLAY_DETECT = 4
|
||||
ISC_REQ_SEQUENCE_DETECT = 8
|
||||
ISC_REQ_CONFIDENTIALITY = 16
|
||||
ISC_REQ_USE_SESSION_KEY = 32
|
||||
ISC_REQ_PROMPT_FOR_CREDS = 64
|
||||
ISC_REQ_USE_SUPPLIED_CREDS = 128
|
||||
ISC_REQ_ALLOCATE_MEMORY = 256
|
||||
ISC_REQ_USE_DCE_STYLE = 512
|
||||
ISC_REQ_DATAGRAM = 1024
|
||||
ISC_REQ_CONNECTION = 2048
|
||||
ISC_REQ_EXTENDED_ERROR = 16384
|
||||
ISC_REQ_STREAM = 32768
|
||||
ISC_REQ_INTEGRITY = 65536
|
||||
ISC_REQ_MANUAL_CRED_VALIDATION = 524288
|
||||
ISC_REQ_HTTP = 268435456
|
||||
|
||||
ASC_REQ_DELEGATE = 1
|
||||
ASC_REQ_MUTUAL_AUTH = 2
|
||||
ASC_REQ_REPLAY_DETECT = 4
|
||||
ASC_REQ_SEQUENCE_DETECT = 8
|
||||
ASC_REQ_CONFIDENTIALITY = 16
|
||||
ASC_REQ_USE_SESSION_KEY = 32
|
||||
ASC_REQ_ALLOCATE_MEMORY = 256
|
||||
ASC_REQ_USE_DCE_STYLE = 512
|
||||
ASC_REQ_DATAGRAM = 1024
|
||||
ASC_REQ_CONNECTION = 2048
|
||||
ASC_REQ_EXTENDED_ERROR = 32768
|
||||
ASC_REQ_STREAM = 65536
|
||||
ASC_REQ_INTEGRITY = 131072
|
||||
)
|
||||
|
||||
type CtxtHandle struct {
|
||||
Lower uintptr
|
||||
Upper uintptr
|
||||
}
|
||||
|
||||
type SecBuffer struct {
|
||||
BufferSize uint32
|
||||
BufferType uint32
|
||||
Buffer *byte
|
||||
}
|
||||
|
||||
type SecBufferDesc struct {
|
||||
Version uint32
|
||||
BuffersCount uint32
|
||||
Buffers *SecBuffer
|
||||
}
|
||||
|
||||
//sys InitializeSecurityContext(credential *CredHandle, context *CtxtHandle, targname *uint16, contextreq uint32, reserved1 uint32, targdatarep uint32, input *SecBufferDesc, reserved2 uint32, newcontext *CtxtHandle, output *SecBufferDesc, contextattr *uint32, expiry *syscall.Filetime) (ret syscall.Errno) = secur32.InitializeSecurityContextW
|
||||
//sys AcceptSecurityContext(credential *CredHandle, context *CtxtHandle, input *SecBufferDesc, contextreq uint32, targdatarep uint32, newcontext *CtxtHandle, output *SecBufferDesc, contextattr *uint32, expiry *syscall.Filetime) (ret syscall.Errno) = secur32.AcceptSecurityContext
|
||||
//sys CompleteAuthToken(context *CtxtHandle, token *SecBufferDesc) (ret syscall.Errno) = secur32.CompleteAuthToken
|
||||
//sys DeleteSecurityContext(context *CtxtHandle) (ret syscall.Errno) = secur32.DeleteSecurityContext
|
||||
//sys ImpersonateSecurityContext(context *CtxtHandle) (ret syscall.Errno) = secur32.ImpersonateSecurityContext
|
||||
//sys RevertSecurityContext(context *CtxtHandle) (ret syscall.Errno) = secur32.RevertSecurityContext
|
||||
//sys QueryContextAttributes(context *CtxtHandle, attribute uint32, buf *byte) (ret syscall.Errno) = secur32.QueryContextAttributesW
|
||||
//sys EncryptMessage(context *CtxtHandle, qop uint32, message *SecBufferDesc, messageseqno uint32) (ret syscall.Errno) = secur32.EncryptMessage
|
||||
//sys DecryptMessage(context *CtxtHandle, message *SecBufferDesc, messageseqno uint32, qop *uint32) (ret syscall.Errno) = secur32.DecryptMessage
|
||||
//sys ApplyControlToken(context *CtxtHandle, input *SecBufferDesc) (ret syscall.Errno) = secur32.ApplyControlToken
|
||||
//sys MakeSignature(context *CtxtHandle, qop uint32, message *SecBufferDesc, messageseqno uint32) (ret syscall.Errno) = secur32.MakeSignature
|
||||
//sys VerifySignature(context *CtxtHandle, message *SecBufferDesc, messageseqno uint32, qop *uint32) (ret syscall.Errno) = secur32.VerifySignature
|
|
@ -0,0 +1,152 @@
|
|||
// MACHINE GENERATED BY 'go generate' COMMAND; DO NOT EDIT
|
||||
|
||||
package sspi
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var _ unsafe.Pointer
|
||||
|
||||
// Do the interface allocations only once for common
|
||||
// Errno values.
|
||||
const (
|
||||
errnoERROR_IO_PENDING = 997
|
||||
)
|
||||
|
||||
var (
|
||||
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
|
||||
)
|
||||
|
||||
// errnoErr returns common boxed Errno values, to prevent
|
||||
// allocations at runtime.
|
||||
func errnoErr(e syscall.Errno) error {
|
||||
switch e {
|
||||
case 0:
|
||||
return nil
|
||||
case errnoERROR_IO_PENDING:
|
||||
return errERROR_IO_PENDING
|
||||
}
|
||||
// TODO: add more here, after collecting data on the common
|
||||
// error values see on Windows. (perhaps when running
|
||||
// all.bat?)
|
||||
return e
|
||||
}
|
||||
|
||||
var (
|
||||
modsecur32 = syscall.NewLazyDLL("secur32.dll")
|
||||
|
||||
procQuerySecurityPackageInfoW = modsecur32.NewProc("QuerySecurityPackageInfoW")
|
||||
procFreeContextBuffer = modsecur32.NewProc("FreeContextBuffer")
|
||||
procAcquireCredentialsHandleW = modsecur32.NewProc("AcquireCredentialsHandleW")
|
||||
procFreeCredentialsHandle = modsecur32.NewProc("FreeCredentialsHandle")
|
||||
procInitializeSecurityContextW = modsecur32.NewProc("InitializeSecurityContextW")
|
||||
procAcceptSecurityContext = modsecur32.NewProc("AcceptSecurityContext")
|
||||
procCompleteAuthToken = modsecur32.NewProc("CompleteAuthToken")
|
||||
procDeleteSecurityContext = modsecur32.NewProc("DeleteSecurityContext")
|
||||
procImpersonateSecurityContext = modsecur32.NewProc("ImpersonateSecurityContext")
|
||||
procRevertSecurityContext = modsecur32.NewProc("RevertSecurityContext")
|
||||
procQueryContextAttributesW = modsecur32.NewProc("QueryContextAttributesW")
|
||||
procEncryptMessage = modsecur32.NewProc("EncryptMessage")
|
||||
procDecryptMessage = modsecur32.NewProc("DecryptMessage")
|
||||
procApplyControlToken = modsecur32.NewProc("ApplyControlToken")
|
||||
procMakeSignature = modsecur32.NewProc("MakeSignature")
|
||||
procVerifySignature = modsecur32.NewProc("VerifySignature")
|
||||
)
|
||||
|
||||
func QuerySecurityPackageInfo(pkgname *uint16, pkginfo **SecPkgInfo) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procQuerySecurityPackageInfoW.Addr(), 2, uintptr(unsafe.Pointer(pkgname)), uintptr(unsafe.Pointer(pkginfo)), 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func FreeContextBuffer(buf *byte) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procFreeContextBuffer.Addr(), 1, uintptr(unsafe.Pointer(buf)), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func AcquireCredentialsHandle(principal *uint16, pkgname *uint16, creduse uint32, logonid *LUID, authdata *byte, getkeyfn uintptr, getkeyarg uintptr, handle *CredHandle, expiry *syscall.Filetime) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall9(procAcquireCredentialsHandleW.Addr(), 9, uintptr(unsafe.Pointer(principal)), uintptr(unsafe.Pointer(pkgname)), uintptr(creduse), uintptr(unsafe.Pointer(logonid)), uintptr(unsafe.Pointer(authdata)), uintptr(getkeyfn), uintptr(getkeyarg), uintptr(unsafe.Pointer(handle)), uintptr(unsafe.Pointer(expiry)))
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func FreeCredentialsHandle(handle *CredHandle) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procFreeCredentialsHandle.Addr(), 1, uintptr(unsafe.Pointer(handle)), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func InitializeSecurityContext(credential *CredHandle, context *CtxtHandle, targname *uint16, contextreq uint32, reserved1 uint32, targdatarep uint32, input *SecBufferDesc, reserved2 uint32, newcontext *CtxtHandle, output *SecBufferDesc, contextattr *uint32, expiry *syscall.Filetime) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall12(procInitializeSecurityContextW.Addr(), 12, uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(targname)), uintptr(contextreq), uintptr(reserved1), uintptr(targdatarep), uintptr(unsafe.Pointer(input)), uintptr(reserved2), uintptr(unsafe.Pointer(newcontext)), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(contextattr)), uintptr(unsafe.Pointer(expiry)))
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func AcceptSecurityContext(credential *CredHandle, context *CtxtHandle, input *SecBufferDesc, contextreq uint32, targdatarep uint32, newcontext *CtxtHandle, output *SecBufferDesc, contextattr *uint32, expiry *syscall.Filetime) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall9(procAcceptSecurityContext.Addr(), 9, uintptr(unsafe.Pointer(credential)), uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(input)), uintptr(contextreq), uintptr(targdatarep), uintptr(unsafe.Pointer(newcontext)), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(contextattr)), uintptr(unsafe.Pointer(expiry)))
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func CompleteAuthToken(context *CtxtHandle, token *SecBufferDesc) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procCompleteAuthToken.Addr(), 2, uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(token)), 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func DeleteSecurityContext(context *CtxtHandle) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procDeleteSecurityContext.Addr(), 1, uintptr(unsafe.Pointer(context)), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func ImpersonateSecurityContext(context *CtxtHandle) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procImpersonateSecurityContext.Addr(), 1, uintptr(unsafe.Pointer(context)), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func RevertSecurityContext(context *CtxtHandle) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procRevertSecurityContext.Addr(), 1, uintptr(unsafe.Pointer(context)), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func QueryContextAttributes(context *CtxtHandle, attribute uint32, buf *byte) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procQueryContextAttributesW.Addr(), 3, uintptr(unsafe.Pointer(context)), uintptr(attribute), uintptr(unsafe.Pointer(buf)))
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func EncryptMessage(context *CtxtHandle, qop uint32, message *SecBufferDesc, messageseqno uint32) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall6(procEncryptMessage.Addr(), 4, uintptr(unsafe.Pointer(context)), uintptr(qop), uintptr(unsafe.Pointer(message)), uintptr(messageseqno), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func DecryptMessage(context *CtxtHandle, message *SecBufferDesc, messageseqno uint32, qop *uint32) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall6(procDecryptMessage.Addr(), 4, uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(message)), uintptr(messageseqno), uintptr(unsafe.Pointer(qop)), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func ApplyControlToken(context *CtxtHandle, input *SecBufferDesc) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall(procApplyControlToken.Addr(), 2, uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(input)), 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func MakeSignature(context *CtxtHandle, qop uint32, message *SecBufferDesc, messageseqno uint32) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall6(procMakeSignature.Addr(), 4, uintptr(unsafe.Pointer(context)), uintptr(qop), uintptr(unsafe.Pointer(message)), uintptr(messageseqno), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
||||
|
||||
func VerifySignature(context *CtxtHandle, message *SecBufferDesc, messageseqno uint32, qop *uint32) (ret syscall.Errno) {
|
||||
r0, _, _ := syscall.Syscall6(procVerifySignature.Addr(), 4, uintptr(unsafe.Pointer(context)), uintptr(unsafe.Pointer(message)), uintptr(messageseqno), uintptr(unsafe.Pointer(qop)), 0, 0)
|
||||
ret = syscall.Errno(r0)
|
||||
return
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
dist: xenial
|
||||
sudo: false
|
||||
language: go
|
||||
|
||||
services:
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
- Existing `HMSet` is renamed to `HSet` and old deprecated `HMSet` is restored for Redis 3 users.
|
||||
|
||||
## v7.1
|
||||
|
||||
- Existing `Cmd.String` is renamed to `Cmd.Text`. New `Cmd.String` implements `fmt.Stringer` interface.
|
||||
|
||||
## v7
|
||||
|
||||
- *Important*. Tx.Pipeline now returns a non-transactional pipeline. Use Tx.TxPipeline for a transactional pipeline.
|
||||
|
|
|
@ -101,7 +101,7 @@ set, err := client.SetNX("key", "value", 10*time.Second).Result()
|
|||
vals, err := client.Sort("list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result()
|
||||
|
||||
// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2
|
||||
vals, err := client.ZRangeByScoreWithScores("zset", redis.ZRangeBy{
|
||||
vals, err := client.ZRangeByScoreWithScores("zset", &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: "+inf",
|
||||
Offset: 0,
|
||||
|
@ -109,7 +109,10 @@ vals, err := client.ZRangeByScoreWithScores("zset", redis.ZRangeBy{
|
|||
}).Result()
|
||||
|
||||
// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM
|
||||
vals, err := client.ZInterStore("out", redis.ZStore{Weights: []int64{2, 3}}, "zset1", "zset2").Result()
|
||||
vals, err := client.ZInterStore("out", &redis.ZStore{
|
||||
Keys: []string{"zset1", "zset2"},
|
||||
Weights: []int64{2, 3}
|
||||
}).Result()
|
||||
|
||||
// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello"
|
||||
vals, err := client.Eval("return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result()
|
||||
|
|
|
@ -57,6 +57,7 @@ type ClusterOptions struct {
|
|||
|
||||
OnConnect func(*Conn) error
|
||||
|
||||
Username string
|
||||
Password string
|
||||
|
||||
MaxRetries int
|
||||
|
@ -67,6 +68,9 @@ type ClusterOptions struct {
|
|||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// NewClient creates a cluster node client with provided name and options.
|
||||
NewClient func(opt *Options) *Client
|
||||
|
||||
// PoolSize applies per cluster node and not for the whole cluster.
|
||||
PoolSize int
|
||||
MinIdleConns int
|
||||
|
@ -118,6 +122,10 @@ func (opt *ClusterOptions) init() {
|
|||
case 0:
|
||||
opt.MaxRetryBackoff = 512 * time.Millisecond
|
||||
}
|
||||
|
||||
if opt.NewClient == nil {
|
||||
opt.NewClient = NewClient
|
||||
}
|
||||
}
|
||||
|
||||
func (opt *ClusterOptions) clientOptions() *Options {
|
||||
|
@ -130,6 +138,7 @@ func (opt *ClusterOptions) clientOptions() *Options {
|
|||
MaxRetries: opt.MaxRetries,
|
||||
MinRetryBackoff: opt.MinRetryBackoff,
|
||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
readOnly: opt.ReadOnly,
|
||||
|
||||
|
@ -162,7 +171,7 @@ func newClusterNode(clOpt *ClusterOptions, addr string) *clusterNode {
|
|||
opt := clOpt.clientOptions()
|
||||
opt.Addr = addr
|
||||
node := clusterNode{
|
||||
Client: NewClient(opt),
|
||||
Client: clOpt.NewClient(opt),
|
||||
}
|
||||
|
||||
node.latency = math.MaxUint32
|
||||
|
|
|
@ -671,7 +671,7 @@ func (cmd *StringCmd) Time() (time.Time, error) {
|
|||
if cmd.err != nil {
|
||||
return time.Time{}, cmd.err
|
||||
}
|
||||
return time.Parse(time.RFC3339, cmd.Val())
|
||||
return time.Parse(time.RFC3339Nano, cmd.Val())
|
||||
}
|
||||
|
||||
func (cmd *StringCmd) Scan(val interface{}) error {
|
||||
|
|
|
@ -302,6 +302,7 @@ type Cmdable interface {
|
|||
type StatefulCmdable interface {
|
||||
Cmdable
|
||||
Auth(password string) *StatusCmd
|
||||
AuthACL(username, password string) *StatusCmd
|
||||
Select(index int) *StatusCmd
|
||||
SwapDB(index1, index2 int) *StatusCmd
|
||||
ClientSetName(name string) *BoolCmd
|
||||
|
@ -324,6 +325,15 @@ func (c statefulCmdable) Auth(password string) *StatusCmd {
|
|||
return cmd
|
||||
}
|
||||
|
||||
// Perform an AUTH command, using the given user and pass.
|
||||
// Should be used to authenticate the current connection with one of the connections defined in the ACL list
|
||||
// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
|
||||
func (c statefulCmdable) AuthACL(username, password string) *StatusCmd {
|
||||
cmd := NewStatusCmd("auth", username, password)
|
||||
_ = c(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Echo(message interface{}) *StringCmd {
|
||||
cmd := NewStringCmd("echo", message)
|
||||
_ = c(cmd)
|
||||
|
|
|
@ -6,9 +6,24 @@ import (
|
|||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v7/internal/pool"
|
||||
"github.com/go-redis/redis/v7/internal/proto"
|
||||
)
|
||||
|
||||
var ErrClosed = pool.ErrClosed
|
||||
|
||||
type Error interface {
|
||||
error
|
||||
|
||||
// RedisError is a no-op function but
|
||||
// serves to distinguish types that are Redis
|
||||
// errors from ordinary errors: a type is a
|
||||
// Redis error if it has a RedisError method.
|
||||
RedisError()
|
||||
}
|
||||
|
||||
var _ Error = proto.RedisError("")
|
||||
|
||||
func isRetryableError(err error, retryTimeout bool) bool {
|
||||
switch err {
|
||||
case nil, context.Canceled, context.DeadlineExceeded:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue