fix(caldav): Incoming tasks do not get correct time zone (#1455)
Dates from tasks.org may be formatted like DUE;TZID=Europe/Berlin:20230402T150000 After this fix the parameter TZID is no longer ignored and the Vikunja task gets a DueDate of 13:00 UTC, which corresponds to 15:00 in Europe/Berlin. Before this fix, the time was parsed to 15:00 UTC. Resolves https://kolaente.dev/vikunja/api/issues/1453 Co-authored-by: ce72 <christoph.ernst72@googlemail.com> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/1455 Reviewed-by: konrad <k@knt.li> Co-authored-by: cernst <ce72@noreply.kolaente.de> Co-committed-by: cernst <ce72@noreply.kolaente.de>
This commit is contained in:
parent
f45648a6f7
commit
1cffef6908
@ -22,6 +22,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
"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/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
@ -88,15 +89,15 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
return nil, errors.New("VTODO element not found")
|
return nil, errors.New("VTODO element not found")
|
||||||
}
|
}
|
||||||
// We put the vTodo details in a map to be able to handle them more easily
|
// We put the vTodo details in a map to be able to handle them more easily
|
||||||
task := make(map[string]string)
|
task := make(map[string]ics.IANAProperty)
|
||||||
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
|
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
|
||||||
task[c.IANAToken] = c.Value
|
task[c.IANAToken] = c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the priority
|
// Parse the priority
|
||||||
var priority int64
|
var priority int64
|
||||||
if _, ok := task["PRIORITY"]; ok {
|
if _, ok := task["PRIORITY"]; ok {
|
||||||
priorityParsed, err := strconv.ParseInt(task["PRIORITY"], 10, 64)
|
priorityParsed, err := strconv.ParseInt(task["PRIORITY"].Value, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -105,14 +106,14 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse the enddate
|
// Parse the enddate
|
||||||
duration, _ := time.ParseDuration(task["DURATION"])
|
duration, _ := time.ParseDuration(task["DURATION"].Value)
|
||||||
|
|
||||||
description := strings.ReplaceAll(task["DESCRIPTION"], "\\,", ",")
|
description := strings.ReplaceAll(task["DESCRIPTION"].Value, "\\,", ",")
|
||||||
description = strings.ReplaceAll(description, "\\n", "\n")
|
description = strings.ReplaceAll(description, "\\n", "\n")
|
||||||
|
|
||||||
var labels []*models.Label
|
var labels []*models.Label
|
||||||
if val, ok := task["CATEGORIES"]; ok {
|
if val, ok := task["CATEGORIES"]; ok {
|
||||||
categories := strings.Split(val, ",")
|
categories := strings.Split(val.Value, ",")
|
||||||
labels = make([]*models.Label, 0, len(categories))
|
labels = make([]*models.Label, 0, len(categories))
|
||||||
for _, category := range categories {
|
for _, category := range categories {
|
||||||
labels = append(labels, &models.Label{
|
labels = append(labels, &models.Label{
|
||||||
@ -122,8 +123,8 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vTask = &models.Task{
|
vTask = &models.Task{
|
||||||
UID: task["UID"],
|
UID: task["UID"].Value,
|
||||||
Title: task["SUMMARY"],
|
Title: task["SUMMARY"].Value,
|
||||||
Description: description,
|
Description: description,
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
@ -133,7 +134,7 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
|
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if task["STATUS"] == "COMPLETED" {
|
if task["STATUS"].Value == "COMPLETED" {
|
||||||
vTask.Done = true
|
vTask.Done = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +160,7 @@ func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) *models.Task {
|
|||||||
if contains(property.ICalParameters["VALUE"], "DATE-TIME") {
|
if contains(property.ICalParameters["VALUE"], "DATE-TIME") {
|
||||||
// Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
// Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
||||||
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||||
Reminder: caldavTimeToTimestamp(property.Value),
|
Reminder: caldavTimeToTimestamp(property),
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -199,7 +200,8 @@ func contains(array []string, str string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc5545#section-3.3.5
|
// https://tools.ietf.org/html/rfc5545#section-3.3.5
|
||||||
func caldavTimeToTimestamp(tstring string) time.Time {
|
func caldavTimeToTimestamp(ianaProperty ics.IANAProperty) time.Time {
|
||||||
|
tstring := ianaProperty.Value
|
||||||
if tstring == "" {
|
if tstring == "" {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
@ -214,7 +216,24 @@ func caldavTimeToTimestamp(tstring string) time.Time {
|
|||||||
format = `20060102`
|
format = `20060102`
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := time.Parse(format, tstring)
|
var t time.Time
|
||||||
|
var err error
|
||||||
|
tzParameter := ianaProperty.ICalParameters["TZID"]
|
||||||
|
if len(tzParameter) > 0 {
|
||||||
|
loc, err := time.LoadLocation(tzParameter[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error while parsing caldav timezone %s: %s", tzParameter[0], err)
|
||||||
|
} else {
|
||||||
|
t, err = time.ParseInLocation(format, tstring, loc)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s at location %s", tstring, loc, err)
|
||||||
|
} else {
|
||||||
|
t = t.In(config.GetTimeZone())
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t, err = time.Parse(format, tstring)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err)
|
log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err)
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
|
@ -219,6 +219,76 @@ END:VCALENDAR`,
|
|||||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "example task from tasks.org app",
|
||||||
|
args: args{content: `BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:+//IDN tasks.org//android-130102//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
DTSTAMP:20230402T074158Z
|
||||||
|
UID:4290517349243274514
|
||||||
|
CREATED:20230402T060451Z
|
||||||
|
LAST-MODIFIED:20230402T074154Z
|
||||||
|
SUMMARY:Test with tasks.org
|
||||||
|
PRIORITY:9
|
||||||
|
CATEGORIES:Vikunja
|
||||||
|
X-APPLE-SORT-ORDER:697384109
|
||||||
|
DUE;TZID=Europe/Berlin:20230402T170001
|
||||||
|
DTSTART;TZID=Europe/Berlin:20230401T090000
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;RELATED=END:PT0S
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Default Tasks.org description
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;VALUE=DATE-TIME:20230402T100000Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Default Tasks.org description
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
BEGIN:VTIMEZONE
|
||||||
|
TZID:Europe/Berlin
|
||||||
|
LAST-MODIFIED:20220816T024022Z
|
||||||
|
BEGIN:DAYLIGHT
|
||||||
|
TZNAME:CEST
|
||||||
|
TZOFFSETFROM:+0100
|
||||||
|
TZOFFSETTO:+0200
|
||||||
|
DTSTART:19810329T020000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||||
|
END:DAYLIGHT
|
||||||
|
BEGIN:STANDARD
|
||||||
|
TZNAME:CET
|
||||||
|
TZOFFSETFROM:+0200
|
||||||
|
TZOFFSETTO:+0100
|
||||||
|
DTSTART:19961027T030000
|
||||||
|
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||||
|
END:STANDARD
|
||||||
|
END:VTIMEZONE
|
||||||
|
END:VCALENDAR`,
|
||||||
|
},
|
||||||
|
wantVTask: &models.Task{
|
||||||
|
Updated: time.Date(2023, 4, 2, 7, 41, 58, 0, config.GetTimeZone()),
|
||||||
|
UID: "4290517349243274514",
|
||||||
|
Title: "Test with tasks.org",
|
||||||
|
Priority: 1,
|
||||||
|
Labels: []*models.Label{
|
||||||
|
{
|
||||||
|
Title: "Vikunja",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DueDate: time.Date(2023, 4, 2, 15, 0, 1, 0, config.GetTimeZone()),
|
||||||
|
StartDate: time.Date(2023, 4, 1, 7, 0, 0, 0, config.GetTimeZone()),
|
||||||
|
Reminders: []*models.TaskReminder{
|
||||||
|
{
|
||||||
|
RelativeTo: models.ReminderRelationDueDate,
|
||||||
|
RelativePeriod: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Reminder: time.Date(2023, 4, 2, 10, 0, 0, 0, config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user