From e21a3904ffdd8b7b7d26a40ad8601709e77a3250 Mon Sep 17 00:00:00 2001 From: kolaente Date: Sun, 11 Jul 2021 15:03:50 +0200 Subject: [PATCH] Fix mapping task priorities from Vikunja to calDAV Resolves #866 --- pkg/caldav/caldav.go | 2 +- pkg/caldav/caldav_test.go | 33 ++++++ pkg/{routes => }/caldav/parsing.go | 21 ++-- pkg/caldav/parsing_test.go | 101 +++++++++++++++++ pkg/caldav/priority.go | 66 ++++++++++++ pkg/caldav/priority_test.go | 131 +++++++++++++++++++++++ pkg/routes/caldav/handler.go | 3 +- pkg/routes/caldav/listStorageProvider.go | 10 +- 8 files changed, 350 insertions(+), 17 deletions(-) rename pkg/{routes => }/caldav/parsing.go (87%) create mode 100644 pkg/caldav/parsing_test.go create mode 100644 pkg/caldav/priority.go create mode 100644 pkg/caldav/priority_test.go diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index 59c1149a0eb..7c79d614e5f 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -216,7 +216,7 @@ DURATION:PT` + fmt.Sprintf("%.6f", t.Duration.Hours()) + `H` + fmt.Sprintf("%.6f if t.Priority != 0 { caldavtodos += ` -PRIORITY:` + strconv.Itoa(int(t.Priority)) +PRIORITY:` + strconv.Itoa(mapPriorityToCaldav(t.Priority)) } caldavtodos += ` diff --git a/pkg/caldav/caldav_test.go b/pkg/caldav/caldav_test.go index 7c9a5260333..b78e8bed067 100644 --- a/pkg/caldav/caldav_test.go +++ b/pkg/caldav/caldav_test.go @@ -375,6 +375,39 @@ COMPLETED:20181201T013024 STATUS:COMPLETED LAST-MODIFIED:00010101T000000 END:VTODO +END:VCALENDAR`, + }, + { + name: "with priority", + args: args{ + config: &Config{ + Name: "test", + ProdID: "RandomProdID which is not random", + }, + todos: []*Todo{ + { + Summary: "Todo #1", + Description: "Lorem Ipsum", + UID: "randommduid", + Priority: 1, + Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()), + }, + }, + }, + wantCaldavtasks: `BEGIN:VCALENDAR +VERSION:2.0 +METHOD:PUBLISH +X-PUBLISHED-TTL:PT4H +X-WR-CALNAME:test +PRODID:-//RandomProdID which is not random//EN +BEGIN:VTODO +UID:randommduid +DTSTAMP:20181201T011204 +SUMMARY:Todo #1 +DESCRIPTION:Lorem Ipsum +PRIORITY:9 +LAST-MODIFIED:00010101T000000 +END:VTODO END:VCALENDAR`, }, } diff --git a/pkg/routes/caldav/parsing.go b/pkg/caldav/parsing.go similarity index 87% rename from pkg/routes/caldav/parsing.go rename to pkg/caldav/parsing.go index 083762db6b1..0b566743447 100644 --- a/pkg/routes/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -21,21 +21,20 @@ import ( "strings" "time" - "code.vikunja.io/api/pkg/caldav" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "github.com/laurent22/ical-go" ) -func getCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string { +func GetCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string { // Make caldav todos from Vikunja todos - var caldavtodos []*caldav.Todo + var caldavtodos []*Todo for _, t := range listTasks { duration := t.EndDate.Sub(t.StartDate) - caldavtodos = append(caldavtodos, &caldav.Todo{ + caldavtodos = append(caldavtodos, &Todo{ Timestamp: t.Updated, UID: t.UID, Summary: t.Title, @@ -52,15 +51,15 @@ func getCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string }) } - caldavConfig := &caldav.Config{ + caldavConfig := &Config{ Name: list.Title, ProdID: "Vikunja Todo App", } - return caldav.ParseTodos(caldavConfig, caldavtodos) + return ParseTodos(caldavConfig, caldavtodos) } -func parseTaskFromVTODO(content string) (vTask *models.Task, err error) { +func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { parsed, err := ical.ParseCalendar(content) if err != nil { return nil, err @@ -78,13 +77,15 @@ func parseTaskFromVTODO(content string) (vTask *models.Task, err error) { } } - // Parse the UID + // Parse the priority var priority int64 if _, ok := task["PRIORITY"]; ok { - priority, err = strconv.ParseInt(task["PRIORITY"], 10, 64) + priorityParsed, err := strconv.ParseInt(task["PRIORITY"], 10, 64) if err != nil { return nil, err } + + priority = parseVTODOPriority(priorityParsed) } // Parse the enddate @@ -118,7 +119,7 @@ func caldavTimeToTimestamp(tstring string) time.Time { return time.Time{} } - format := caldav.DateFormat + format := DateFormat if strings.HasSuffix(tstring, "Z") { format = `20060102T150405Z` diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go new file mode 100644 index 00000000000..99bb61a6267 --- /dev/null +++ b/pkg/caldav/parsing_test.go @@ -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 . + +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) + } + }) + } +} diff --git a/pkg/caldav/priority.go b/pkg/caldav/priority.go new file mode 100644 index 00000000000..0e39c749b93 --- /dev/null +++ b/pkg/caldav/priority.go @@ -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 . + +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 +} diff --git a/pkg/caldav/priority_test.go b/pkg/caldav/priority_test.go new file mode 100644 index 00000000000..6e59f58ff20 --- /dev/null +++ b/pkg/caldav/priority_test.go @@ -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 . + +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) + } + }) + } +} diff --git a/pkg/routes/caldav/handler.go b/pkg/routes/caldav/handler.go index 3d413f28593..8d3c4bb7654 100644 --- a/pkg/routes/caldav/handler.go +++ b/pkg/routes/caldav/handler.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" + caldav2 "code.vikunja.io/api/pkg/caldav" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/user" @@ -66,7 +67,7 @@ func ListHandler(c echo.Context) error { // Parse it vtodo := string(body) if vtodo != "" && strings.HasPrefix(vtodo, `BEGIN:VCALENDAR`) { - storage.task, err = parseTaskFromVTODO(vtodo) + storage.task, err = caldav2.ParseTaskFromVTODO(vtodo) if err != nil { log.Error(err) return echo.ErrInternalServerError diff --git a/pkg/routes/caldav/listStorageProvider.go b/pkg/routes/caldav/listStorageProvider.go index 1171aa47d62..9abe0341424 100644 --- a/pkg/routes/caldav/listStorageProvider.go +++ b/pkg/routes/caldav/listStorageProvider.go @@ -21,8 +21,8 @@ import ( "strings" "time" + "code.vikunja.io/api/pkg/caldav" "code.vikunja.io/api/pkg/db" - "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" user2 "code.vikunja.io/api/pkg/user" @@ -256,7 +256,7 @@ func (vcls *VikunjaCaldavListStorage) CreateResource(rpath, content string) (*da s := db.NewSession() defer s.Close() - vTask, err := parseTaskFromVTODO(content) + vTask, err := caldav.ParseTaskFromVTODO(content) if err != nil { return nil, err } @@ -295,7 +295,7 @@ func (vcls *VikunjaCaldavListStorage) CreateResource(rpath, content string) (*da // UpdateResource updates a resource func (vcls *VikunjaCaldavListStorage) UpdateResource(rpath, content string) (*data.Resource, error) { - vTask, err := parseTaskFromVTODO(content) + vTask, err := caldav.ParseTaskFromVTODO(content) if err != nil { 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) func (vlra *VikunjaListResourceAdapter) GetContent() string { 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 { list := models.List{Tasks: []*models.Task{vlra.task}} - return getCaldavTodosForTasks(&list, list.Tasks) + return caldav.GetCaldavTodosForTasks(&list, list.Tasks) } return ""