Fix mapping task priorities from Vikunja to calDAV

Resolves #866
This commit is contained in:
kolaente 2021-07-11 15:03:50 +02:00
parent 562ef9af36
commit e21a3904ff
No known key found for this signature in database
GPG key ID: F40E70337AB24C9B
8 changed files with 350 additions and 17 deletions

View file

@ -216,7 +216,7 @@ DURATION:PT` + fmt.Sprintf("%.6f", t.Duration.Hours()) + `H` + fmt.Sprintf("%.6f
if t.Priority != 0 { if t.Priority != 0 {
caldavtodos += ` caldavtodos += `
PRIORITY:` + strconv.Itoa(int(t.Priority)) PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority))
} }
caldavtodos += ` caldavtodos += `

View file

@ -375,6 +375,39 @@ COMPLETED:20181201T013024
STATUS:COMPLETED STATUS:COMPLETED
LAST-MODIFIED:00010101T000000 LAST-MODIFIED:00010101T000000
END:VTODO END:VTODO
END:VCALENDAR`,
},
{
name: "with priority",
args: args{
config: &Config{
Name: "test",
ProdID: "RandomProdID which is not random",
},
todos: []*Todo{
{
Summary: "Todo #1",
Description: "Lorem Ipsum",
UID: "randommduid",
Priority: 1,
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
},
wantCaldavtasks: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randommduid
DTSTAMP:20181201T011204
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
PRIORITY:9
LAST-MODIFIED:00010101T000000
END:VTODO
END:VCALENDAR`, END:VCALENDAR`,
}, },
} }

View file

@ -21,21 +21,20 @@ import (
"strings" "strings"
"time" "time"
"code.vikunja.io/api/pkg/caldav"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"github.com/laurent22/ical-go" "github.com/laurent22/ical-go"
) )
func getCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string { func GetCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string {
// Make caldav todos from Vikunja todos // Make caldav todos from Vikunja todos
var caldavtodos []*caldav.Todo var caldavtodos []*Todo
for _, t := range listTasks { for _, t := range listTasks {
duration := t.EndDate.Sub(t.StartDate) duration := t.EndDate.Sub(t.StartDate)
caldavtodos = append(caldavtodos, &caldav.Todo{ caldavtodos = append(caldavtodos, &Todo{
Timestamp: t.Updated, Timestamp: t.Updated,
UID: t.UID, UID: t.UID,
Summary: t.Title, Summary: t.Title,
@ -52,15 +51,15 @@ func getCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string
}) })
} }
caldavConfig := &caldav.Config{ caldavConfig := &Config{
Name: list.Title, Name: list.Title,
ProdID: "Vikunja Todo App", ProdID: "Vikunja Todo App",
} }
return caldav.ParseTodos(caldavConfig, caldavtodos) return ParseTodos(caldavConfig, caldavtodos)
} }
func parseTaskFromVTODO(content string) (vTask *models.Task, err error) { func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
parsed, err := ical.ParseCalendar(content) parsed, err := ical.ParseCalendar(content)
if err != nil { if err != nil {
return nil, err return nil, err
@ -78,13 +77,15 @@ func parseTaskFromVTODO(content string) (vTask *models.Task, err error) {
} }
} }
// Parse the UID // Parse the priority
var priority int64 var priority int64
if _, ok := task["PRIORITY"]; ok { if _, ok := task["PRIORITY"]; ok {
priority, err = strconv.ParseInt(task["PRIORITY"], 10, 64) priorityParsed, err := strconv.ParseInt(task["PRIORITY"], 10, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
priority = parseVTODOPriority(priorityParsed)
} }
// Parse the enddate // Parse the enddate
@ -118,7 +119,7 @@ func caldavTimeToTimestamp(tstring string) time.Time {
return time.Time{} return time.Time{}
} }
format := caldav.DateFormat format := DateFormat
if strings.HasSuffix(tstring, "Z") { if strings.HasSuffix(tstring, "Z") {
format = `20060102T150405Z` format = `20060102T150405Z`

101
pkg/caldav/parsing_test.go Normal file
View file

@ -0,0 +1,101 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package caldav
import (
"testing"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/models"
"gopkg.in/d4l3k/messagediff.v1"
)
func TestParseTaskFromVTODO(t *testing.T) {
type args struct {
content string
}
tests := []struct {
name string
args args
wantVTask *models.Task
wantErr bool
}{
{
name: "normal",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011204
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
LAST-MODIFIED:00010101T000000
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "Todo #1",
UID: "randomuid",
Description: "Lorem Ipsum",
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
{
name: "With priority",
args: args{content: `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:test
PRODID:-//RandomProdID which is not random//EN
BEGIN:VTODO
UID:randomuid
DTSTAMP:20181201T011204
SUMMARY:Todo #1
DESCRIPTION:Lorem Ipsum
PRIORITY:9
LAST-MODIFIED:00010101T000000
END:VTODO
END:VCALENDAR`,
},
wantVTask: &models.Task{
Title: "Todo #1",
UID: "randomuid",
Description: "Lorem Ipsum",
Priority: 1,
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseTaskFromVTODO(tt.args.content)
if (err != nil) != tt.wantErr {
t.Errorf("ParseTaskFromVTODO() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff, equal := messagediff.PrettyDiff(got, tt.wantVTask); !equal {
t.Errorf("ParseTaskFromVTODO() gotVTask = %v, want %v, diff = %s", got, tt.wantVTask, diff)
}
})
}
}

66
pkg/caldav/priority.go Normal file
View file

@ -0,0 +1,66 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package caldav
// In caldav, priority values are an int from 0 to 9 where 1 is the highest priority and 9 the lowest. 0 is "unset".
// Vikunja only has priorites from 0 to 5 where 0 is unset and 5 is the highest
// See https://icalendar.org/iCalendar-RFC-5545/3-8-1-9-priority.html
func mapPriorityToCaldav(priority int64) (caldavPriority int) {
switch priority {
case 0:
return 0
case 1: // Low
return 9
case 2: // Medium
return 5
case 3: // High
return 3
case 4: // Urgent
return 2
case 5: // DO NOW
return 1
}
return 0
}
// See mapPriorityToCaldav
func parseVTODOPriority(priority int64) (vikunjaPriority int64) {
switch priority {
case 0:
return 0
case 1:
return 5
case 2:
return 4
case 3:
return 3
case 4:
return 3
case 5:
return 2
case 6:
return 1
case 7:
return 1
case 8:
return 1
case 9:
return 1
}
return 0
}

131
pkg/caldav/priority_test.go Normal file
View file

@ -0,0 +1,131 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package caldav
import "testing"
func Test_parseVTODOPriority(t *testing.T) {
tests := []struct {
name string
priority int64
want int64
}{
{
name: "unset",
priority: 0,
want: 0,
},
{
name: "DO NOW",
priority: 1,
want: 5,
},
{
name: "urgent",
priority: 2,
want: 4,
},
{
name: "high 1",
priority: 3,
want: 3,
},
{
name: "high 2",
priority: 4,
want: 3,
},
{
name: "medium",
priority: 5,
want: 2,
},
{
name: "low 1",
priority: 6,
want: 1,
},
{
name: "low 2",
priority: 7,
want: 1,
},
{
name: "low 3",
priority: 8,
want: 1,
},
{
name: "low 4",
priority: 9,
want: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotVikunjaPriority := parseVTODOPriority(tt.priority); gotVikunjaPriority != tt.want {
t.Errorf("parseVTODOPriority() = %v, want %v", gotVikunjaPriority, tt.want)
}
})
}
}
func Test_mapPriorityToCaldav(t *testing.T) {
tests := []struct {
name string
priority int64
wantCaldavPriority int
}{
{
name: "unset",
priority: 0,
wantCaldavPriority: 0,
},
{
name: "low",
priority: 1,
wantCaldavPriority: 9,
},
{
name: "medium",
priority: 2,
wantCaldavPriority: 5,
},
{
name: "high",
priority: 3,
wantCaldavPriority: 3,
},
{
name: "urgent",
priority: 4,
wantCaldavPriority: 2,
},
{
name: "DO NOW",
priority: 5,
wantCaldavPriority: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotCaldavPriority := mapPriorityToCaldav(tt.priority); gotCaldavPriority != tt.wantCaldavPriority {
t.Errorf("mapPriorityToCaldav() = %v, want %v", gotCaldavPriority, tt.wantCaldavPriority)
}
})
}
}

View file

@ -24,6 +24,7 @@ import (
"strconv" "strconv"
"strings" "strings"
caldav2 "code.vikunja.io/api/pkg/caldav"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
@ -66,7 +67,7 @@ func ListHandler(c echo.Context) error {
// Parse it // Parse it
vtodo := string(body) vtodo := string(body)
if vtodo != "" && strings.HasPrefix(vtodo, `BEGIN:VCALENDAR`) { if vtodo != "" && strings.HasPrefix(vtodo, `BEGIN:VCALENDAR`) {
storage.task, err = parseTaskFromVTODO(vtodo) storage.task, err = caldav2.ParseTaskFromVTODO(vtodo)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return echo.ErrInternalServerError return echo.ErrInternalServerError

View file

@ -21,8 +21,8 @@ import (
"strings" "strings"
"time" "time"
"code.vikunja.io/api/pkg/caldav"
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
user2 "code.vikunja.io/api/pkg/user" user2 "code.vikunja.io/api/pkg/user"
@ -256,7 +256,7 @@ func (vcls *VikunjaCaldavListStorage) CreateResource(rpath, content string) (*da
s := db.NewSession() s := db.NewSession()
defer s.Close() defer s.Close()
vTask, err := parseTaskFromVTODO(content) vTask, err := caldav.ParseTaskFromVTODO(content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -295,7 +295,7 @@ func (vcls *VikunjaCaldavListStorage) CreateResource(rpath, content string) (*da
// UpdateResource updates a resource // UpdateResource updates a resource
func (vcls *VikunjaCaldavListStorage) UpdateResource(rpath, content string) (*data.Resource, error) { func (vcls *VikunjaCaldavListStorage) UpdateResource(rpath, content string) (*data.Resource, error) {
vTask, err := parseTaskFromVTODO(content) vTask, err := caldav.ParseTaskFromVTODO(content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -411,12 +411,12 @@ func (vlra *VikunjaListResourceAdapter) CalculateEtag() string {
// GetContent returns the content string of a resource (a task in our case) // GetContent returns the content string of a resource (a task in our case)
func (vlra *VikunjaListResourceAdapter) GetContent() string { func (vlra *VikunjaListResourceAdapter) GetContent() string {
if vlra.list != nil && vlra.list.Tasks != nil { if vlra.list != nil && vlra.list.Tasks != nil {
return getCaldavTodosForTasks(vlra.list, vlra.listTasks) return caldav.GetCaldavTodosForTasks(vlra.list, vlra.listTasks)
} }
if vlra.task != nil { if vlra.task != nil {
list := models.List{Tasks: []*models.Task{vlra.task}} list := models.List{Tasks: []*models.Task{vlra.task}}
return getCaldavTodosForTasks(&list, list.Tasks) return caldav.GetCaldavTodosForTasks(&list, list.Tasks)
} }
return "" return ""