forked from vikunja/vikunja
Better caldav support (#73)
This commit is contained in:
parent
de24fcc2f8
commit
7107d030fc
@ -26,6 +26,8 @@ steps:
|
||||
- name: build
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
environment:
|
||||
GOFLAGS: '-mod=vendor'
|
||||
commands:
|
||||
- make lint
|
||||
- make fmt-check
|
||||
|
@ -48,10 +48,10 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||
* [x] Pagination
|
||||
* Sollte in der Config definierbar sein, wie viel pro Seite angezeigt werden soll, die CRUD-Methoden übergeben dann ein "gibt mir die Seite sowieso" an die CRUDable-Funktionenen, die müssen das dann Auswerten. Geht leider nicht anders, wenn man erst 2342352 Einträge hohlt und die dann nachträglich auf 200 begrenzt ist das ne massive Ressourcenverschwendung.
|
||||
* [x] Testen, ob man über die Routen methode von echo irgendwie ein swagger spec generieren könnte -> Andere Swagger library
|
||||
* [ ] CalDAV
|
||||
* [x] CalDAV
|
||||
* [x] Basics
|
||||
* [x] Reminders
|
||||
* [ ] Discovery, stichwort PROPFIND
|
||||
* [x] Discovery, stichwort PROPFIND
|
||||
* [x] Wir brauchen noch ne gute idee, wie man die listen kriegt, auf die man nur so Zugriff hat (ohne namespace)
|
||||
* Dazu am Besten nen pseudonamespace anlegen (id -1 oder so), der hat das dann alles
|
||||
* [x] Testing mit locust: https://locust.io/
|
||||
@ -171,7 +171,6 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||
* [ ] Some kind of milestones for tasks
|
||||
* [ ] Create tasks from a text/markdown file (probably frontend only)
|
||||
* [ ] Label-view: Get a bunch of tasks by label
|
||||
* [ ] Better caldav support (VTODO)
|
||||
* [ ] Debian package should have a service file
|
||||
* [ ] Downloads should be served via nginx (with theme?), minio should only be used for pushing artifacts.
|
||||
* [ ] User struct should have a field for the avatar url (-> gravatar md5 calculated by the backend)
|
||||
@ -179,6 +178,34 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||
-> Check if there's a way to do that efficently. Maybe only implementing it in the web handler.
|
||||
* [ ] List stats to see how many tasks are done, how many are there in total, how many people have acces to a list etc
|
||||
* [ ] Colors for tasks
|
||||
* [ ] Better caldav support
|
||||
* [x] VTODO
|
||||
* [x] Fix organizer prop
|
||||
* [x] Depricate the events thing for now
|
||||
* [x] PROPFIND/OPTIONS : caldav discovery
|
||||
* [x] Create new tasks
|
||||
* [x] Save uid from the client
|
||||
* [x] Update tasks
|
||||
* [x] Marking as done
|
||||
* [x] Fix OPTIONS Requests to the rest of the api being broken
|
||||
* [x] Parse all props defined in rfc5545
|
||||
* [x] COMPLETED -> Need to actually save the time the task was completed
|
||||
* [x] Whenever a task ist created/updated, update the `updated` prop on the list so the etag changes and clients get notified
|
||||
* [x] Fix not all tasks being displayed (My guess: Something about that f*cking etag)
|
||||
* [x] Delete tasks
|
||||
* [x] Last modified
|
||||
* [x] Content Size
|
||||
* [x] Modify the caldav lib as proposed in the pr
|
||||
* [x] Improve login performance, each request taking > 1.5 sec is just too much, maybe just use the default value for hash iterations in the login/register function
|
||||
* [x] Only show priority when we have one
|
||||
* [x] Show a proper calendar title
|
||||
* [x] Fix home principal propfind stuff
|
||||
* [x] Docs
|
||||
* [x] Setting to disable caldav completely
|
||||
* [ ] Make it work with the app
|
||||
* [ ] Cleanup the whole mess I made with the handlers and storage providers etc -> Probably a good idea to create a seperate storage provider etc for lists and tasks
|
||||
* [ ] Tests
|
||||
* [ ] Check if only needed things are queried from the db when accessing dav (for ex. no need to get all tasks when we act
|
||||
|
||||
### Refactor
|
||||
|
||||
@ -217,6 +244,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||
|
||||
### Later
|
||||
|
||||
* [ ] Backgrounds for lists -> needs uploading and storing and so on
|
||||
* [ ] Plugins
|
||||
* [ ] Rename Namespaces?
|
||||
* [ ] Namespaces to collections and n-n (one list can be in multiple collections)?
|
||||
@ -258,6 +286,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||
* [ ] Nozbe
|
||||
* [ ] Lanes
|
||||
* [ ] Nirvana
|
||||
* [ ] Any.do
|
||||
* [ ] Good ol' Caldav (Tasks)
|
||||
* [ ] More auth providers
|
||||
* [ ] LDAP/AD
|
||||
|
@ -16,6 +16,8 @@ service:
|
||||
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
|
||||
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||
enablemetrics: false
|
||||
# Enable the caldav endpoint, see the docs for more details
|
||||
enablecaldav: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql and sqlite.
|
||||
|
@ -59,6 +59,8 @@ service:
|
||||
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
|
||||
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||
enablemetrics: false
|
||||
# Enable the caldav endpoint, see the docs for more details
|
||||
enablecaldav: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql and sqlite.
|
||||
|
140
docs/content/doc/usage/caldav.md
Normal file
140
docs/content/doc/usage/caldav.md
Normal file
@ -0,0 +1,140 @@
|
||||
---
|
||||
date: "2019-05-12:00:00+01:00"
|
||||
title: "Caldav"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Caldav
|
||||
|
||||
> **Warning:** The caldav integration is in an early alpha stage and has bugs.
|
||||
> It works well with some clients while having issues with others.
|
||||
> If you encounter issues, please [report them](https://code.vikunja.io/api/issues/new?body=[caldav])
|
||||
|
||||
Vikunja supports managing tasks via the [caldav VTODO](https://tools.ietf.org/html/rfc5545#section-3.6.2) extension.
|
||||
|
||||
## URLs
|
||||
|
||||
All urls are located under the `/dav` subspace.
|
||||
|
||||
Urls are:
|
||||
|
||||
* `/principals/<username>/`: Returns urls for list discovery. *Use this url to initially make connections to new clients.*
|
||||
* `/lists/`: Used to manage lists
|
||||
* `/lists/<List ID>/`: Used to manage a single list
|
||||
* `/lists/<List ID>/<Task UID>`: Used to manage a task on a list
|
||||
|
||||
## Supported properties
|
||||
|
||||
Vikunja currently supports the following properties:
|
||||
|
||||
* `UID`
|
||||
* `SUMMARY`
|
||||
* `DESCRIPTION`
|
||||
* `PRIORITY`
|
||||
* `COMPLETED`
|
||||
* `DUE`
|
||||
* `DTSTART`
|
||||
* `DURATION`
|
||||
* `ORGANIZER`
|
||||
* `RELATED-TO`
|
||||
* `CREATED`
|
||||
* `DTSTAMP`
|
||||
* `LAST-MODIFIED`
|
||||
|
||||
Vikunja **currently does not** support these properties:
|
||||
|
||||
* `ATTACH`
|
||||
* `CATEGORIES`
|
||||
* `CLASS`
|
||||
* `COMMENT`
|
||||
* `GEO`
|
||||
* `LOCATION`
|
||||
* `PERCENT-COMPLETE`
|
||||
* `RESOURCES`
|
||||
* `STATUS`
|
||||
* `CONTACT`
|
||||
* `RECURRENCE-ID`
|
||||
* `URL`
|
||||
* Recurrence
|
||||
* `SEQUENCE`
|
||||
|
||||
## Tested Clients
|
||||
|
||||
#### Working
|
||||
|
||||
* [Evolution](https://wiki.gnome.org/Apps/Evolution/)
|
||||
|
||||
#### Not working
|
||||
|
||||
* [Tasks (Android)](https://tasks.org/)
|
||||
|
||||
## Dev logs
|
||||
|
||||
The whole thing is not optimized at all and probably pretty inefficent.
|
||||
|
||||
Request body and headers are logged if the debug output is enabled.
|
||||
|
||||
```
|
||||
Creating a new task:
|
||||
PUT /dav/lists/1/cd4dd0e1b3c19cc9d787829b6e08be536e3df3a4.ics
|
||||
|
||||
Body:
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTODO
|
||||
UID:cd4dd0e1b3c19cc9d787829b6e08be536e3df3a4
|
||||
DTSTAMP:20190508T134538Z
|
||||
SUMMARY:test2000
|
||||
PRIORITY:0
|
||||
CLASS:PUBLIC
|
||||
CREATED:20190508T134710Z
|
||||
LAST-MODIFIED:20190508T134710Z
|
||||
END:VTODO
|
||||
END:VCALENDAR
|
||||
|
||||
|
||||
Marking a task as done:
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTODO
|
||||
UID:3ada92f28b4ceda38562ebf047c6ff05400d4c572352a
|
||||
DTSTAMP:20190511T183631
|
||||
DTSTART:19700101T000000
|
||||
DTEND:19700101T000000
|
||||
SUMMARY:sdgs
|
||||
ORGANIZER;CN=:user
|
||||
CREATED:20190511T183631
|
||||
PRIORITY:0
|
||||
LAST-MODIFIED:20190512T193428Z
|
||||
COMPLETED:20190512T193428Z
|
||||
PERCENT-COMPLETE:100
|
||||
STATUS:COMPLETED
|
||||
END:VTODO
|
||||
END:VCALENDAR
|
||||
|
||||
Requests from the app:::
|
||||
|
||||
[CALDAV] Request Body: <?xml version="1.0" encoding="UTF-8" ?><propfind xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav"><prop><current-user-principal /></prop></propfind>
|
||||
[CALDAV] GetResources: rpath: /dav/
|
||||
2019-05-18T23:25:49.971140654+02:00: WEB ▶ 192.168.1.134 PROPFIND 207 /dav/ 1.021705664s - okhttp/3.12.2
|
||||
|
||||
[CALDAV] Request Body: <?xml version="1.0" encoding="UTF-8" ?><propfind xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav"><prop><CAL:calendar-home-set /></prop></propfind>
|
||||
[CALDAV] GetResources: rpath: /dav/
|
||||
2019-05-18T23:25:52.166996113+02:00: WEB ▶ 192.168.1.134 PROPFIND 207 /dav/ 1.042834467s - okhttp/3.12.2
|
||||
|
||||
And then it just stops.
|
||||
... and complains about not being able to find the home set
|
||||
... without even requesting it...
|
||||
|
||||
|
||||
```
|
8
go.mod
8
go.mod
@ -21,6 +21,7 @@ require (
|
||||
code.vikunja.io/web v0.0.0-20190507193736-edb39812af9c
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/client9/misspell v0.3.4
|
||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
@ -43,6 +44,7 @@ require (
|
||||
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb
|
||||
github.com/labstack/echo/v4 v4.1.5
|
||||
github.com/labstack/gommon v0.2.8
|
||||
github.com/laurent22/ical-go v0.1.0
|
||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 // indirect
|
||||
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.4 // indirect
|
||||
@ -53,6 +55,7 @@ require (
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/prometheus/client_golang v0.9.2
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
@ -71,3 +74,8 @@ require (
|
||||
src.techknowlogick.com/xgo v0.0.0-20190507142556-a5b29ecb0ff4
|
||||
src.techknowlogick.com/xormigrate v0.0.0-20190321151057-24497c23c09c
|
||||
)
|
||||
|
||||
//replace (
|
||||
// github.com/labstack/echo/v4 => ../../github.com/labstack/echo // Branch: feature/report-method, PR https://github.com/labstack/echo/pull/1332
|
||||
// github.com/samedi/caldav-go => ../../github.com/samedi/caldav-go // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6
|
||||
//)
|
||||
|
8
go.sum
8
go.sum
@ -16,6 +16,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
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 v0.0.0-20171015221209-af219c0c7ea1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
@ -118,6 +121,9 @@ github.com/labstack/echo/v4 v4.1.5 h1:RztCXCvfMljychg0G/IzW5T7hL6ADqqwREwcX279Q1
|
||||
github.com/labstack/echo/v4 v4.1.5/go.mod h1:3LbYC6VkwmUnmLPZ8WFdHdQHG77e9GQbjyhWdb1QvC4=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/laurent22/ical-go v0.0.0-20170824131750-e4fec3492969/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/laurent22/ical-go v0.1.0 h1:4vZHBD3/+ne+IN+c3B+v6d9Ff8+70pzTjCWsjfDRvL0=
|
||||
github.com/laurent22/ical-go v0.1.0/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
@ -175,6 +181,8 @@ github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jO
|
||||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible h1:OrYCCUPvQTyGybtf64G232NPkWuQRl5hc2CC6rQNT/U=
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible/go.mod h1:zY411fqgoxSEcCL75WxwchM+8p4n/MnRUPfVusQke3Q=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
|
@ -17,11 +17,16 @@
|
||||
package caldav
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DateFormat ist the caldav date format
|
||||
const DateFormat = `20060102T150405`
|
||||
|
||||
// Event holds a single caldav event
|
||||
type Event struct {
|
||||
Summary string
|
||||
@ -34,6 +39,29 @@ type Event struct {
|
||||
EndUnix int64
|
||||
}
|
||||
|
||||
// Todo holds a single VTODO
|
||||
type Todo struct {
|
||||
// Required
|
||||
TimestampUnix int64
|
||||
UID string
|
||||
|
||||
// Optional
|
||||
Summary string
|
||||
Description string
|
||||
CompletedUnix int64
|
||||
Organizer *models.User
|
||||
Priority int64 // 0-9, 1 is highest
|
||||
RelatedToUID string
|
||||
|
||||
StartUnix int64
|
||||
EndUnix int64
|
||||
DueDateUnix int64
|
||||
Duration time.Duration
|
||||
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64 // last-mod
|
||||
}
|
||||
|
||||
// Alarm holds infos about an alarm from a caldav event
|
||||
type Alarm struct {
|
||||
TimeUnix int64
|
||||
@ -92,10 +120,89 @@ END:VCALENDAR` // Need a line break
|
||||
return
|
||||
}
|
||||
|
||||
// ParseTodos returns a caldav vcalendar string with todos
|
||||
func ParseTodos(config *Config, todos []*Todo) (caldavtodos string) {
|
||||
caldavtodos = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:` + config.Name + `
|
||||
PRODID:-//` + config.ProdID + `//EN`
|
||||
|
||||
for _, t := range todos {
|
||||
if t.UID == "" {
|
||||
t.UID = makeCalDavTimeFromUnixTime(t.TimestampUnix) + utils.Sha256(t.Summary)
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
BEGIN:VTODO
|
||||
UID:` + t.UID + `
|
||||
DTSTAMP:` + makeCalDavTimeFromUnixTime(t.TimestampUnix) + `
|
||||
SUMMARY:` + t.Summary
|
||||
|
||||
if t.StartUnix != 0 {
|
||||
caldavtodos += `
|
||||
DTSTART: ` + makeCalDavTimeFromUnixTime(t.StartUnix)
|
||||
}
|
||||
if t.EndUnix != 0 {
|
||||
caldavtodos += `
|
||||
DTEND: ` + makeCalDavTimeFromUnixTime(t.EndUnix)
|
||||
}
|
||||
if t.Description != "" {
|
||||
caldavtodos += `
|
||||
DESCRIPTION:` + t.Description
|
||||
}
|
||||
if t.CompletedUnix != 0 {
|
||||
caldavtodos += `
|
||||
COMPLETED: ` + makeCalDavTimeFromUnixTime(t.CompletedUnix)
|
||||
}
|
||||
if t.Organizer != nil {
|
||||
caldavtodos += `
|
||||
ORGANIZER;CN=:` + t.Organizer.Username
|
||||
}
|
||||
|
||||
if t.RelatedToUID != "" {
|
||||
caldavtodos += `
|
||||
RELATED-TO:` + t.RelatedToUID
|
||||
}
|
||||
|
||||
if t.DueDateUnix != 0 {
|
||||
caldavtodos += `
|
||||
DUE:` + makeCalDavTimeFromUnixTime(t.DueDateUnix)
|
||||
}
|
||||
|
||||
if t.CreatedUnix != 0 {
|
||||
caldavtodos += `
|
||||
CREATED:` + makeCalDavTimeFromUnixTime(t.CreatedUnix)
|
||||
}
|
||||
|
||||
if t.Duration != 0 {
|
||||
caldavtodos += `
|
||||
DURATION:PT` + fmt.Sprintf("%.6f", t.Duration.Hours()) + `H` + fmt.Sprintf("%.6f", t.Duration.Minutes()) + `M` + fmt.Sprintf("%.6f", t.Duration.Seconds()) + `S`
|
||||
}
|
||||
|
||||
if t.Priority != 0 {
|
||||
caldavtodos += `
|
||||
PRIORITY:` + strconv.Itoa(int(t.Priority))
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
LAST-MODIFIED:` + makeCalDavTimeFromUnixTime(t.UpdatedUnix)
|
||||
|
||||
caldavtodos += `
|
||||
END:VTODO`
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
END:VCALENDAR` // Need a line break
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeCalDavTimeFromUnixTime(unixtime int64) (caldavtime string) {
|
||||
tz, _ := time.LoadLocation("UTC")
|
||||
tm := time.Unix(unixtime, 0).In(tz)
|
||||
return tm.Format("20060102T150405")
|
||||
return tm.Format(DateFormat)
|
||||
}
|
||||
|
||||
func calcAlarmDateFromReminder(eventStartUnix, reminderUnix int64) (alarmTime string) {
|
||||
|
@ -40,6 +40,7 @@ func InitConfig() {
|
||||
viper.SetDefault("service.JWTSecret", random)
|
||||
viper.SetDefault("service.interface", ":3456")
|
||||
viper.SetDefault("service.frontendurl", "")
|
||||
viper.SetDefault("service.enablecaldav", true)
|
||||
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
|
@ -80,33 +80,33 @@ func TestListTask(t *testing.T) {
|
||||
t.Run("by priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priority"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"prioritydesc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priorityasc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
// should equal duedate desc
|
||||
t.Run("by duedate", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadate"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadatedesc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedateasc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
|
84
pkg/migration/20190511202210.go
Normal file
84
pkg/migration/20190511202210.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 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/utils"
|
||||
"github.com/go-xorm/xorm"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
)
|
||||
|
||||
type listTask20190511202210 struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
Text string `xorm:"varchar(250) not null" json:"text" valid:"runelength(3|250)" minLength:"3" maxLength:"250"`
|
||||
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
|
||||
Done bool `xorm:"INDEX null" json:"done"`
|
||||
DoneAtUnix int64 `xorm:"INDEX null" json:"doneAt"`
|
||||
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"`
|
||||
RemindersUnix []int64 `xorm:"JSON TEXT null" json:"reminderDates"`
|
||||
CreatedByID int64 `xorm:"int(11) not null" json:"-"` // ID of the user who put that task on the list
|
||||
ListID int64 `xorm:"int(11) INDEX not null" json:"listID" param:"list"`
|
||||
RepeatAfter int64 `xorm:"int(11) INDEX null" json:"repeatAfter"`
|
||||
ParentTaskID int64 `xorm:"int(11) INDEX null" json:"parentTaskID"`
|
||||
Priority int64 `xorm:"int(11) null" json:"priority"`
|
||||
StartDateUnix int64 `xorm:"int(11) INDEX null" json:"startDate" query:"-"`
|
||||
EndDateUnix int64 `xorm:"int(11) INDEX null" json:"endDate" query:"-"`
|
||||
HexColor string `xorm:"varchar(6) null" json:"hexColor" valid:"runelength(0|6)" maxLength:"6"`
|
||||
UID string `xorm:"varchar(250) null" json:"-"`
|
||||
Sorting string `xorm:"-" json:"-" query:"sort"` // Parameter to sort by
|
||||
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
|
||||
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
|
||||
Created int64 `xorm:"created not null" json:"created"`
|
||||
Updated int64 `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (listTask20190511202210) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20190511202210",
|
||||
Description: "Add task uid",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(listTask20190511202210{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all tasks and generate a random uid for them
|
||||
var allTasks []*listTask20190511202210
|
||||
err = tx.Find(&allTasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range allTasks {
|
||||
t.UID = utils.MakeRandomString(40)
|
||||
_, err = tx.Where("id = ?", t.ID).Cols("uid").Update(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return dropTableColum(tx, "tasks", "uid")
|
||||
},
|
||||
})
|
||||
}
|
43
pkg/migration/20190514192749.go
Normal file
43
pkg/migration/20190514192749.go
Normal file
@ -0,0 +1,43 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 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 (
|
||||
"github.com/go-xorm/xorm"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
)
|
||||
|
||||
type listTask20190514192749 struct {
|
||||
DoneAtUnix int64 `xorm:"INDEX null" json:"doneAt"`
|
||||
}
|
||||
|
||||
func (listTask20190514192749) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20190514192749",
|
||||
Description: "Add task done at",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(listTask20190514192749{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return dropTableColum(tx, "tasks", "done_at_unix")
|
||||
},
|
||||
})
|
||||
}
|
@ -86,6 +86,11 @@ func (lt *LabelTask) Create(a web.Auth) (err error) {
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(lt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListByTaskID(lt.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
@ -272,6 +277,8 @@ func (t *ListTask) updateTaskLabels(creator web.Auth, labels []*Label) (err erro
|
||||
}
|
||||
t.Labels = append(t.Labels, label)
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
@ -299,7 +306,7 @@ type LabelTaskBulk struct {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/labels/bulk [post]
|
||||
func (ltb *LabelTaskBulk) Create(a web.Auth) (err error) {
|
||||
task, err := GetListTaskByID(ltb.TaskID)
|
||||
task, err := GetTaskByID(ltb.TaskID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ func (ltb *LabelTaskBulk) CanCreate(a web.Auth) (bool, error) {
|
||||
// always the same check for either deleting or adding a label to a task
|
||||
func canDoLabelTask(taskID int64, a web.Auth) (bool, error) {
|
||||
// A user can add a label to a task if he can write to the task
|
||||
task, err := getTaskByIDSimple(taskID)
|
||||
task, err := GetTaskByIDSimple(taskID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -166,6 +166,8 @@ func TestLabelTask_Create(t *testing.T) {
|
||||
a: &User{ID: 1},
|
||||
},
|
||||
wantForbidden: true,
|
||||
wantErr: true,
|
||||
errType: IsErrListTaskDoesNotExist,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@ -71,6 +71,21 @@ func (l *List) Update() (err error) {
|
||||
return CreateOrUpdateList(l)
|
||||
}
|
||||
|
||||
func updateListLastUpdated(list *List) error {
|
||||
_, err := x.ID(list.ID).Cols("updated").Update(list)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateListByTaskID(taskID int64) (err error) {
|
||||
// need to get the task to update the list last updated timestamp
|
||||
task, err := GetTaskByIDSimple(taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateListLastUpdated(&List{ID: task.ListID})
|
||||
}
|
||||
|
||||
// Create implements the create method of CRUDable
|
||||
// @Summary Creates a new list
|
||||
// @Description Creates a new list in a given namespace. The user needs write-access to the namespace.
|
||||
|
@ -125,6 +125,8 @@ func (t *ListTask) updateTaskAssignees(assignees []*User) (err error) {
|
||||
}
|
||||
|
||||
t.setTaskAssignees(assignees)
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
@ -152,6 +154,11 @@ func (t *ListTask) setTaskAssignees(assignees []*User) {
|
||||
// @Router /tasks/{taskID}/assignees/{userID} [delete]
|
||||
func (la *ListTaskAssginee) Delete() (err error) {
|
||||
_, err = x.Delete(&ListTaskAssginee{TaskID: la.TaskID, UserID: la.UserID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListByTaskID(la.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
@ -198,7 +205,11 @@ func (t *ListTask) addNewAssigneeByID(newAssigneeID int64, list *List) (err erro
|
||||
TaskID: t.ID,
|
||||
UserID: newAssigneeID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
@ -249,7 +260,7 @@ type BulkAssignees struct {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/assignees/bulk [post]
|
||||
func (ba *BulkAssignees) Create(a web.Auth) (err error) {
|
||||
task, err := GetListTaskByID(ba.TaskID) // We need to use the full method here because we need all current assignees.
|
||||
task, err := GetTaskByID(ba.TaskID) // We need to use the full method here because we need all current assignees.
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ type ListTask struct {
|
||||
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
|
||||
// Whether a task is done or not.
|
||||
Done bool `xorm:"INDEX null" json:"done"`
|
||||
// The unix timestamp when a task was marked as done.
|
||||
DoneAtUnix int64 `xorm:"INDEX null" json:"doneAt"`
|
||||
// A unix timestamp when the task is due.
|
||||
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"`
|
||||
// An array of unix timestamps when the user wants to be reminded of the task.
|
||||
@ -55,6 +57,9 @@ type ListTask struct {
|
||||
// The task color in hex
|
||||
HexColor string `xorm:"varchar(6) null" json:"hexColor" valid:"runelength(0|6)" maxLength:"6"`
|
||||
|
||||
// The UID is currently not used for anything other than caldav, which is why we don't expose it over json
|
||||
UID string `xorm:"varchar(250) null" json:"-"`
|
||||
|
||||
Sorting string `xorm:"-" json:"-" query:"sort"` // Parameter to sort by
|
||||
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
|
||||
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
|
||||
@ -88,96 +93,36 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// No need to iterate over users and stuff if the list doesn't has tasks
|
||||
if len(taskMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all users & task ids and put them into the array
|
||||
var userIDs []int64
|
||||
var taskIDs []int64
|
||||
for _, i := range taskMap {
|
||||
taskIDs = append(taskIDs, i.ID)
|
||||
userIDs = append(userIDs, i.CreatedByID)
|
||||
}
|
||||
|
||||
// Get all assignees
|
||||
taskAssignees, err := getRawTaskAssigneesForTasks(taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Put the assignees in the task map
|
||||
for _, a := range taskAssignees {
|
||||
if a != nil {
|
||||
taskMap[a.TaskID].Assignees = append(taskMap[a.TaskID].Assignees, &a.User)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all labels for the tasks
|
||||
labels, err := getLabelsByTaskIDs(&LabelByTaskIDsOptions{TaskIDs: taskIDs})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, l := range labels {
|
||||
if l != nil {
|
||||
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &l.Label)
|
||||
}
|
||||
}
|
||||
|
||||
users := make(map[int64]*User)
|
||||
err = x.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add all user objects to the appropriate tasks
|
||||
for _, task := range taskMap {
|
||||
|
||||
// Make created by user objects
|
||||
taskMap[task.ID].CreatedBy = *users[task.CreatedByID]
|
||||
|
||||
// Reorder all subtasks
|
||||
if task.ParentTaskID != 0 {
|
||||
taskMap[task.ParentTaskID].Subtasks = append(taskMap[task.ParentTaskID].Subtasks, task)
|
||||
delete(taskMap, task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// make a complete slice from the map
|
||||
tasks = []*ListTask{}
|
||||
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 (saved on heap).
|
||||
// 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
|
||||
})
|
||||
|
||||
tasks, err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
func getTaskByIDSimple(taskID int64) (task ListTask, err error) {
|
||||
// GetTaskByIDSimple returns a raw task without extra data by the task ID
|
||||
func GetTaskByIDSimple(taskID int64) (task ListTask, err error) {
|
||||
if taskID < 1 {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
||||
}
|
||||
|
||||
exists, err := x.ID(taskID).Get(&task)
|
||||
return GetTaskSimple(&ListTask{ID: taskID})
|
||||
}
|
||||
|
||||
// GetTaskSimple returns a raw task without extra data
|
||||
func GetTaskSimple(t *ListTask) (task ListTask, err error) {
|
||||
task = *t
|
||||
exists, err := x.Get(&task)
|
||||
if err != nil {
|
||||
return ListTask{}, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
||||
return ListTask{}, ErrListTaskDoesNotExist{t.ID}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetListTaskByID returns all tasks a list has
|
||||
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||
listTask, err = getTaskByIDSimple(listTaskID)
|
||||
// GetTaskByID returns all tasks a list has
|
||||
func GetTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||
listTask, err = GetTaskByIDSimple(listTaskID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -221,37 +166,101 @@ func (bt *BulkTask) GetTasksByIDs() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
err = x.In("id", bt.IDs).Find(&bt.Tasks)
|
||||
taskMap := make(map[int64]*ListTask, len(bt.Tasks))
|
||||
err = x.In("id", bt.IDs).Find(&taskMap)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// We use a map, to avoid looping over two slices at once
|
||||
var usermapids = make(map[int64]bool) // Bool ist just something, doesn't acutually matter
|
||||
for _, list := range bt.Tasks {
|
||||
usermapids[list.CreatedByID] = true
|
||||
}
|
||||
bt.Tasks, err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
// Make a slice from the map
|
||||
var userids []int64
|
||||
for uid := range usermapids {
|
||||
userids = append(userids, uid)
|
||||
}
|
||||
|
||||
// Get all users for the tasks
|
||||
var users []*User
|
||||
err = x.In("id", userids).Find(&users)
|
||||
// GetTasksByUIDs gets all tasks from a bunch of uids
|
||||
func GetTasksByUIDs(uids []string) (tasks []*ListTask, err error) {
|
||||
taskMap := make(map[int64]*ListTask)
|
||||
err = x.In("uid", uids).Find(&taskMap)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
for in, task := range bt.Tasks {
|
||||
for _, u := range users {
|
||||
if task.CreatedByID == u.ID {
|
||||
bt.Tasks[in].CreatedBy = *u
|
||||
}
|
||||
tasks, err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
// 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]*ListTask) (tasks []*ListTask, err error) {
|
||||
|
||||
// No need to iterate over users and stuff if the list doesn't has tasks
|
||||
if len(taskMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all users & task ids and put them into the array
|
||||
var userIDs []int64
|
||||
var taskIDs []int64
|
||||
for _, i := range taskMap {
|
||||
taskIDs = append(taskIDs, i.ID)
|
||||
userIDs = append(userIDs, i.CreatedByID)
|
||||
}
|
||||
|
||||
// Get all assignees
|
||||
taskAssignees, err := getRawTaskAssigneesForTasks(taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Put the assignees in the task map
|
||||
for _, a := range taskAssignees {
|
||||
if a != nil {
|
||||
taskMap[a.TaskID].Assignees = append(taskMap[a.TaskID].Assignees, &a.User)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all labels for all the tasks
|
||||
labels, err := getLabelsByTaskIDs(&LabelByTaskIDsOptions{TaskIDs: taskIDs})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, l := range labels {
|
||||
if l != nil {
|
||||
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &l.Label)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all users of a task
|
||||
// aka the ones who created a task
|
||||
users := make(map[int64]*User)
|
||||
err = x.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add all user objects to the appropriate tasks
|
||||
for _, task := range taskMap {
|
||||
|
||||
// Make created by user objects
|
||||
taskMap[task.ID].CreatedBy = *users[task.CreatedByID]
|
||||
|
||||
// Reorder all subtasks
|
||||
if task.ParentTaskID != 0 {
|
||||
taskMap[task.ParentTaskID].Subtasks = append(taskMap[task.ParentTaskID].Subtasks, task)
|
||||
delete(taskMap, task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// make a complete slice from the map
|
||||
tasks = []*ListTask{}
|
||||
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
|
||||
}
|
||||
|
@ -18,8 +18,10 @@ package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/imdario/mergo"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Create is the implementation to create a list task
|
||||
@ -60,6 +62,11 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate a uuid if we don't already have one
|
||||
if t.UID == "" {
|
||||
t.UID = utils.MakeRandomString(40)
|
||||
}
|
||||
|
||||
t.CreatedByID = u.ID
|
||||
t.CreatedBy = u
|
||||
if _, err = x.Insert(t); err != nil {
|
||||
@ -72,6 +79,8 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.TaskCountKey)
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
@ -91,7 +100,7 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
||||
// @Router /tasks/{id} [post]
|
||||
func (t *ListTask) Update() (err error) {
|
||||
// Check if the task exists
|
||||
ot, err := GetListTaskByID(t.ID)
|
||||
ot, err := GetTaskByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -189,12 +198,20 @@ func (t *ListTask) Update() (err error) {
|
||||
"priority",
|
||||
"start_date_unix",
|
||||
"end_date_unix",
|
||||
"hex_color").
|
||||
"hex_color",
|
||||
"done_at_unix").
|
||||
Update(ot)
|
||||
*t = ot
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
// This helper function updates the reminders and doneAtUnix of the *old* task (since that's the one we're inserting
|
||||
// with updated values into the db)
|
||||
func updateDone(oldTask *ListTask, newTask *ListTask) {
|
||||
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
|
||||
oldTask.DueDateUnix = oldTask.DueDateUnix + oldTask.RepeatAfter // assuming we'll save the old task (merged)
|
||||
@ -205,4 +222,13 @@ func updateDone(oldTask *ListTask, newTask *ListTask) {
|
||||
|
||||
newTask.Done = false
|
||||
}
|
||||
|
||||
// Update the "done at" timestamp
|
||||
if !oldTask.Done && newTask.Done {
|
||||
oldTask.DoneAtUnix = time.Now().Unix()
|
||||
}
|
||||
// When unmarking a task as done, reset the timestamp
|
||||
if oldTask.Done && !newTask.Done {
|
||||
oldTask.DoneAtUnix = 0
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ import (
|
||||
func (t *ListTask) Delete() (err error) {
|
||||
|
||||
// Check if it exists
|
||||
_, err = GetListTaskByID(t.ID)
|
||||
_, err = GetTaskByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -51,5 +51,7 @@ func (t *ListTask) Delete() (err error) {
|
||||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.TaskCountKey)
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func (t *ListTask) CanCreate(a web.Auth) (bool, error) {
|
||||
func (t *ListTask) CanRead(a web.Auth) (canRead bool, err error) {
|
||||
//return t.canDoListTask(a)
|
||||
// Get the task, error out if it doesn't exist
|
||||
*t, err = getTaskByIDSimple(t.ID)
|
||||
*t, err = GetTaskByIDSimple(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -58,7 +58,7 @@ func (t *ListTask) canDoListTask(a web.Auth) (bool, error) {
|
||||
doer := getUserForRights(a)
|
||||
|
||||
// Get the task
|
||||
lI, err := getTaskByIDSimple(t.ID)
|
||||
lI, err := GetTaskByIDSimple(t.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func TestListTask_Create(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was updated
|
||||
li, err := GetListTaskByID(listtask.ID)
|
||||
li, err := GetTaskByID(listtask.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, li.Text, "Test34")
|
||||
|
||||
@ -91,3 +91,18 @@ func TestListTask_Create(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
||||
|
||||
func TestUpdateDone(t *testing.T) {
|
||||
t.Run("marking a task as done", func(t *testing.T) {
|
||||
oldTask := &ListTask{Done: false}
|
||||
newTask := &ListTask{Done: true}
|
||||
updateDone(oldTask, newTask)
|
||||
assert.NotEqual(t, int64(0), oldTask.DoneAtUnix)
|
||||
})
|
||||
t.Run("unmarking a task as done", func(t *testing.T) {
|
||||
oldTask := &ListTask{Done: true}
|
||||
newTask := &ListTask{Done: false}
|
||||
updateDone(oldTask, newTask)
|
||||
assert.Equal(t, int64(0), oldTask.DoneAtUnix)
|
||||
})
|
||||
}
|
||||
|
@ -67,6 +67,10 @@ func (lu *ListUser) Create(a web.Auth) (err error) {
|
||||
|
||||
// Insert user <-> list relation
|
||||
_, err = x.Insert(lu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(l)
|
||||
return
|
||||
}
|
||||
|
@ -51,5 +51,10 @@ func (lu *ListUser) Delete() (err error) {
|
||||
|
||||
_, err = x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Delete(&ListUser{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: lu.ListID})
|
||||
return
|
||||
}
|
||||
|
@ -44,5 +44,10 @@ func (lu *ListUser) Update() (err error) {
|
||||
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
||||
Cols("right").
|
||||
Update(lu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: lu.ListID})
|
||||
return
|
||||
}
|
||||
|
@ -65,5 +65,10 @@ func (tl *TeamList) Create(a web.Auth) (err error) {
|
||||
|
||||
// Insert the new team
|
||||
_, err = x.Insert(tl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(l)
|
||||
return
|
||||
}
|
||||
|
@ -53,6 +53,10 @@ func (tl *TeamList) Delete() (err error) {
|
||||
_, err = x.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Delete(TeamList{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: tl.ListID})
|
||||
return
|
||||
}
|
||||
|
@ -44,5 +44,10 @@ func (tl *TeamList) Update() (err error) {
|
||||
Where("list_id = ? AND team_id = ?", tl.ListID, tl.TeamID).
|
||||
Cols("right").
|
||||
Update(tl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: tl.ListID})
|
||||
return
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ func CreateUser(user User) (newUser User, err error) {
|
||||
|
||||
// HashPassword hashes a password
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 11)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
|
@ -1,89 +0,0 @@
|
||||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2018 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 v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/caldav"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"time"
|
||||