feat(caldav): Sync Reminders / VALARM (#1415)
Co-authored-by: ce72 <christoph.ernst72@googlemail.com> Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/1415 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
9443fb1bd5
commit
f45648a6f7
@ -39,30 +39,31 @@ Vikunja currently supports the following properties:
|
|||||||
* `PRIORITY`
|
* `PRIORITY`
|
||||||
* `CATEGORIES`
|
* `CATEGORIES`
|
||||||
* `COMPLETED`
|
* `COMPLETED`
|
||||||
|
* `CREATED` (only Vikunja -> Client)
|
||||||
* `DUE`
|
* `DUE`
|
||||||
* `DTSTART`
|
|
||||||
* `DURATION`
|
* `DURATION`
|
||||||
* `ORGANIZER`
|
|
||||||
* `RELATED-TO`
|
|
||||||
* `CREATED`
|
|
||||||
* `DTSTAMP`
|
* `DTSTAMP`
|
||||||
* `LAST-MODIFIED`
|
* `DTSTART`
|
||||||
* Recurrence
|
* `LAST-MODIFIED` (only Vikunja -> Client)
|
||||||
|
* `RRULE` (Recurrence) (only Vikunja -> Client)
|
||||||
|
* `VALARM` (Reminders)
|
||||||
|
|
||||||
Vikunja **currently does not** support these properties:
|
Vikunja **currently does not** support these properties:
|
||||||
|
|
||||||
* `ATTACH`
|
* `ATTACH`
|
||||||
* `CLASS`
|
* `CLASS`
|
||||||
* `COMMENT`
|
* `COMMENT`
|
||||||
|
* `CONTACT`
|
||||||
* `GEO`
|
* `GEO`
|
||||||
* `LOCATION`
|
* `LOCATION`
|
||||||
|
* `ORGANIZER` (disabled)
|
||||||
* `PERCENT-COMPLETE`
|
* `PERCENT-COMPLETE`
|
||||||
* `RESOURCES`
|
|
||||||
* `STATUS`
|
|
||||||
* `CONTACT`
|
|
||||||
* `RECURRENCE-ID`
|
* `RECURRENCE-ID`
|
||||||
* `URL`
|
* `RELATED-TO`
|
||||||
|
* `RESOURCES`
|
||||||
* `SEQUENCE`
|
* `SEQUENCE`
|
||||||
|
* `STATUS`
|
||||||
|
* `URL`
|
||||||
|
|
||||||
## Tested Clients
|
## Tested Clients
|
||||||
|
|
||||||
|
@ -31,19 +31,6 @@ import (
|
|||||||
// DateFormat is the caldav date format
|
// DateFormat is the caldav date format
|
||||||
const DateFormat = `20060102T150405`
|
const DateFormat = `20060102T150405`
|
||||||
|
|
||||||
// Event holds a single caldav event
|
|
||||||
type Event struct {
|
|
||||||
Summary string
|
|
||||||
Description string
|
|
||||||
UID string
|
|
||||||
Alarms []Alarm
|
|
||||||
Color string
|
|
||||||
|
|
||||||
Timestamp time.Time
|
|
||||||
Start time.Time
|
|
||||||
End time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Todo holds a single VTODO
|
// Todo holds a single VTODO
|
||||||
type Todo struct {
|
type Todo struct {
|
||||||
// Required
|
// Required
|
||||||
@ -65,6 +52,7 @@ type Todo struct {
|
|||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
RepeatAfter int64
|
RepeatAfter int64
|
||||||
RepeatMode models.TaskRepeatMode
|
RepeatMode models.TaskRepeatMode
|
||||||
|
Alarms []Alarm
|
||||||
|
|
||||||
Created time.Time
|
Created time.Time
|
||||||
Updated time.Time // last-mod
|
Updated time.Time // last-mod
|
||||||
@ -73,6 +61,8 @@ type Todo struct {
|
|||||||
// Alarm holds infos about an alarm from a caldav event
|
// Alarm holds infos about an alarm from a caldav event
|
||||||
type Alarm struct {
|
type Alarm struct {
|
||||||
Time time.Time
|
Time time.Time
|
||||||
|
Duration time.Duration
|
||||||
|
RelativeTo models.ReminderRelation
|
||||||
Description string
|
Description string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,58 +90,6 @@ X-OUTLOOK-COLOR:` + color + `
|
|||||||
X-FUNAMBOL-COLOR:` + color
|
X-FUNAMBOL-COLOR:` + color
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseEvents parses an array of caldav events and gives them back as string
|
|
||||||
func ParseEvents(config *Config, events []*Event) (caldavevents string) {
|
|
||||||
caldavevents += `BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
|
||||||
METHOD:PUBLISH
|
|
||||||
X-PUBLISHED-TTL:PT4H
|
|
||||||
X-WR-CALNAME:` + config.Name + `
|
|
||||||
PRODID:-//` + config.ProdID + `//EN` + getCaldavColor(config.Color)
|
|
||||||
|
|
||||||
for _, e := range events {
|
|
||||||
|
|
||||||
if e.UID == "" {
|
|
||||||
e.UID = makeCalDavTimeFromTimeStamp(e.Timestamp) + utils.Sha256(e.Summary)
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedDescription := ""
|
|
||||||
if e.Description != "" {
|
|
||||||
re := regexp.MustCompile(`\r?\n`)
|
|
||||||
formattedDescription = re.ReplaceAllString(e.Description, "\\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
caldavevents += `
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:` + e.UID + `
|
|
||||||
SUMMARY:` + e.Summary + getCaldavColor(e.Color) + `
|
|
||||||
DESCRIPTION:` + formattedDescription + `
|
|
||||||
DTSTAMP:` + makeCalDavTimeFromTimeStamp(e.Timestamp) + `
|
|
||||||
DTSTART:` + makeCalDavTimeFromTimeStamp(e.Start) + `
|
|
||||||
DTEND:` + makeCalDavTimeFromTimeStamp(e.End)
|
|
||||||
|
|
||||||
for _, a := range e.Alarms {
|
|
||||||
if a.Description == "" {
|
|
||||||
a.Description = e.Summary
|
|
||||||
}
|
|
||||||
|
|
||||||
caldavevents += `
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:` + calcAlarmDateFromReminder(e.Start, a.Time) + `
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:` + a.Description + `
|
|
||||||
END:VALARM`
|
|
||||||
}
|
|
||||||
caldavevents += `
|
|
||||||
END:VEVENT`
|
|
||||||
}
|
|
||||||
|
|
||||||
caldavevents += `
|
|
||||||
END:VCALENDAR` // Need a line break
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatDuration(duration time.Duration) string {
|
func formatDuration(duration time.Duration) string {
|
||||||
seconds := duration.Seconds() - duration.Minutes()*60
|
seconds := duration.Seconds() - duration.Minutes()*60
|
||||||
minutes := duration.Minutes() - duration.Hours()*60
|
minutes := duration.Minutes() - duration.Hours()*60
|
||||||
@ -246,7 +184,7 @@ CATEGORIES:` + strings.Join(t.Categories, ",")
|
|||||||
|
|
||||||
caldavtodos += `
|
caldavtodos += `
|
||||||
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
|
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
|
||||||
|
caldavtodos += ParseAlarms(t.Alarms, t.Summary)
|
||||||
caldavtodos += `
|
caldavtodos += `
|
||||||
END:VTODO`
|
END:VTODO`
|
||||||
}
|
}
|
||||||
@ -257,19 +195,42 @@ END:VCALENDAR` // Need a line break
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseAlarms(alarms []Alarm, taskDescription string) (caldavalarms string) {
|
||||||
|
for _, a := range alarms {
|
||||||
|
if a.Description == "" {
|
||||||
|
a.Description = taskDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
caldavalarms += `
|
||||||
|
BEGIN:VALARM`
|
||||||
|
switch a.RelativeTo {
|
||||||
|
case models.ReminderRelationStartDate:
|
||||||
|
caldavalarms += `
|
||||||
|
TRIGGER;RELATED=START:` + makeCalDavDuration(a.Duration)
|
||||||
|
case models.ReminderRelationEndDate, models.ReminderRelationDueDate:
|
||||||
|
caldavalarms += `
|
||||||
|
TRIGGER;RELATED=END:` + makeCalDavDuration(a.Duration)
|
||||||
|
default:
|
||||||
|
caldavalarms += `
|
||||||
|
TRIGGER;VALUE=DATE-TIME:` + makeCalDavTimeFromTimeStamp(a.Time)
|
||||||
|
}
|
||||||
|
caldavalarms += `
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:` + a.Description + `
|
||||||
|
END:VALARM`
|
||||||
|
}
|
||||||
|
return caldavalarms
|
||||||
|
}
|
||||||
|
|
||||||
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
|
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
|
||||||
return ts.In(time.UTC).Format(DateFormat) + "Z"
|
return ts.In(time.UTC).Format(DateFormat) + "Z"
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcAlarmDateFromReminder(eventStart, reminder time.Time) (alarmTime string) {
|
func makeCalDavDuration(duration time.Duration) (caldavtime string) {
|
||||||
diff := reminder.Sub(eventStart)
|
if duration < 0 {
|
||||||
diffStr := strings.ToUpper(diff.String())
|
duration = duration.Abs()
|
||||||
if diff < 0 {
|
caldavtime = "-"
|
||||||
alarmTime += `-`
|
|
||||||
// We append the - at the beginning of the caldav flag, that would get in the way if the minutes
|
|
||||||
// themselves are also containing it
|
|
||||||
diffStr = diffStr[1:]
|
|
||||||
}
|
}
|
||||||
alarmTime += `PT` + diffStr
|
caldavtime += "PT" + strings.ToUpper(duration.Truncate(time.Millisecond).String())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -26,275 +26,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseEvents(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
config *Config
|
|
||||||
events []*Event
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantCaldavevents string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Test caldavparsing without reminders",
|
|
||||||
args: args{
|
|
||||||
config: &Config{
|
|
||||||
Name: "test",
|
|
||||||
ProdID: "RandomProdID which is not random",
|
|
||||||
Color: "ffffff",
|
|
||||||
},
|
|
||||||
events: []*Event{
|
|
||||||
{
|
|
||||||
Summary: "Event #1",
|
|
||||||
Description: "Lorem Ipsum",
|
|
||||||
UID: "randommduid",
|
|
||||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
|
||||||
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
|
||||||
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
|
|
||||||
Color: "affffe",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Summary: "Event #2",
|
|
||||||
UID: "randommduidd",
|
|
||||||
Timestamp: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
|
||||||
Start: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
|
||||||
End: time.Unix(1543738724, 0).In(config.GetTimeZone()),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Summary: "Event #3 with empty uid",
|
|
||||||
UID: "20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83",
|
|
||||||
Timestamp: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
|
||||||
Start: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
|
||||||
End: time.Unix(1543727000, 0).In(config.GetTimeZone()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantCaldavevents: `BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
|
||||||
METHOD:PUBLISH
|
|
||||||
X-PUBLISHED-TTL:PT4H
|
|
||||||
X-WR-CALNAME:test
|
|
||||||
PRODID:-//RandomProdID which is not random//EN
|
|
||||||
X-APPLE-CALENDAR-COLOR:#ffffffFF
|
|
||||||
X-OUTLOOK-COLOR:#ffffffFF
|
|
||||||
X-FUNAMBOL-COLOR:#ffffffFF
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:randommduid
|
|
||||||
SUMMARY:Event #1
|
|
||||||
X-APPLE-CALENDAR-COLOR:#affffeFF
|
|
||||||
X-OUTLOOK-COLOR:#affffeFF
|
|
||||||
X-FUNAMBOL-COLOR:#affffeFF
|
|
||||||
DESCRIPTION:Lorem Ipsum
|
|
||||||
DTSTAMP:20181201T011204Z
|
|
||||||
DTSTART:20181201T011204Z
|
|
||||||
DTEND:20181201T013024Z
|
|
||||||
END:VEVENT
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:randommduidd
|
|
||||||
SUMMARY:Event #2
|
|
||||||
DESCRIPTION:
|
|
||||||
DTSTAMP:20181202T045844Z
|
|
||||||
DTSTART:20181202T045844Z
|
|
||||||
DTEND:20181202T081844Z
|
|
||||||
END:VEVENT
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
|
|
||||||
SUMMARY:Event #3 with empty uid
|
|
||||||
DESCRIPTION:
|
|
||||||
DTSTAMP:20181202T050024Z
|
|
||||||
DTSTART:20181202T050024Z
|
|
||||||
DTEND:20181202T050320Z
|
|
||||||
END:VEVENT
|
|
||||||
END:VCALENDAR`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test caldavparsing with reminders",
|
|
||||||
args: args{
|
|
||||||
config: &Config{
|
|
||||||
Name: "test2",
|
|
||||||
ProdID: "RandomProdID which is not random",
|
|
||||||
},
|
|
||||||
events: []*Event{
|
|
||||||
{
|
|
||||||
Summary: "Event #1",
|
|
||||||
Description: "Lorem Ipsum",
|
|
||||||
UID: "randommduid",
|
|
||||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
|
||||||
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
|
||||||
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
|
|
||||||
Alarms: []Alarm{
|
|
||||||
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
|
|
||||||
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
|
|
||||||
{Time: time.Unix(1543626024, 0)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Summary: "Event #2",
|
|
||||||
UID: "randommduidd",
|
|
||||||
Timestamp: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
|
||||||
Start: time.Unix(1543726724, 0).In(config.GetTimeZone()),
|
|
||||||
End: time.Unix(1543738724, 0).In(config.GetTimeZone()),
|
|
||||||
Alarms: []Alarm{
|
|
||||||
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
|
|
||||||
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
|
|
||||||
{Time: time.Unix(1543626024, 0).In(config.GetTimeZone())},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Summary: "Event #3 with empty uid",
|
|
||||||
Timestamp: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
|
||||||
Start: time.Unix(1543726824, 0).In(config.GetTimeZone()),
|
|
||||||
End: time.Unix(1543727000, 0).In(config.GetTimeZone()),
|
|
||||||
Alarms: []Alarm{
|
|
||||||
{Time: time.Unix(1543626524, 0).In(config.GetTimeZone())},
|
|
||||||
{Time: time.Unix(1543626224, 0).In(config.GetTimeZone())},
|
|
||||||
{Time: time.Unix(1543626024, 0).In(config.GetTimeZone())},
|
|
||||||
{Time: time.Unix(1543826824, 0).In(config.GetTimeZone())},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Summary: "Event #4 without any",
|
|
||||||
Timestamp: time.Unix(1543726824, 0),
|
|
||||||
Start: time.Unix(1543726824, 0),
|
|
||||||
End: time.Unix(1543727000, 0),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantCaldavevents: `BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
|
||||||
METHOD:PUBLISH
|
|
||||||
X-PUBLISHED-TTL:PT4H
|
|
||||||
X-WR-CALNAME:test2
|
|
||||||
PRODID:-//RandomProdID which is not random//EN
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:randommduid
|
|
||||||
SUMMARY:Event #1
|
|
||||||
DESCRIPTION:Lorem Ipsum
|
|
||||||
DTSTAMP:20181201T011204Z
|
|
||||||
DTSTART:20181201T011204Z
|
|
||||||
DTEND:20181201T013024Z
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT3M20S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #1
|
|
||||||
END:VALARM
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT8M20S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #1
|
|
||||||
END:VALARM
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT11M40S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #1
|
|
||||||
END:VALARM
|
|
||||||
END:VEVENT
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:randommduidd
|
|
||||||
SUMMARY:Event #2
|
|
||||||
DESCRIPTION:
|
|
||||||
DTSTAMP:20181202T045844Z
|
|
||||||
DTSTART:20181202T045844Z
|
|
||||||
DTEND:20181202T081844Z
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT27H50M0S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #2
|
|
||||||
END:VALARM
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT27H55M0S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #2
|
|
||||||
END:VALARM
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT27H58M20S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #2
|
|
||||||
END:VALARM
|
|
||||||
END:VEVENT
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:20181202T050024Z2aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83
|
|
||||||
SUMMARY:Event #3 with empty uid
|
|
||||||
DESCRIPTION:
|
|
||||||
DTSTAMP:20181202T050024Z
|
|
||||||
DTSTART:20181202T050024Z
|
|
||||||
DTEND:20181202T050320Z
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT27H51M40S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #3 with empty uid
|
|
||||||
END:VALARM
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT27H56M40S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #3 with empty uid
|
|
||||||
END:VALARM
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:-PT28H0M0S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #3 with empty uid
|
|
||||||
END:VALARM
|
|
||||||
BEGIN:VALARM
|
|
||||||
TRIGGER:PT27H46M40S
|
|
||||||
ACTION:DISPLAY
|
|
||||||
DESCRIPTION:Event #3 with empty uid
|
|
||||||
END:VALARM
|
|
||||||
END:VEVENT
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:20181202T050024Zae7548ce9556df85038abe90dc674d4741a61ce74d1cf
|
|
||||||
SUMMARY:Event #4 without any
|
|
||||||
DESCRIPTION:
|
|
||||||
DTSTAMP:20181202T050024Z
|
|
||||||
DTSTART:20181202T050024Z
|
|
||||||
DTEND:20181202T050320Z
|
|
||||||
END:VEVENT
|
|
||||||
END:VCALENDAR`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test caldavparsing with multiline description",
|
|
||||||
args: args{
|
|
||||||
config: &Config{
|
|
||||||
Name: "test",
|
|
||||||
ProdID: "RandomProdID which is not random",
|
|
||||||
},
|
|
||||||
events: []*Event{
|
|
||||||
{
|
|
||||||
Summary: "Event #1",
|
|
||||||
Description: `Lorem Ipsum
|
|
||||||
Dolor sit amet`,
|
|
||||||
UID: "randommduid",
|
|
||||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
|
||||||
Start: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
|
||||||
End: time.Unix(1543627824, 0).In(config.GetTimeZone()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantCaldavevents: `BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
|
||||||
METHOD:PUBLISH
|
|
||||||
X-PUBLISHED-TTL:PT4H
|
|
||||||
X-WR-CALNAME:test
|
|
||||||
PRODID:-//RandomProdID which is not random//EN
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:randommduid
|
|
||||||
SUMMARY:Event #1
|
|
||||||
DESCRIPTION:Lorem Ipsum\nDolor sit amet
|
|
||||||
DTSTAMP:20181201T011204Z
|
|
||||||
DTSTART:20181201T011204Z
|
|
||||||
DTEND:20181201T013024Z
|
|
||||||
END:VEVENT
|
|
||||||
END:VCALENDAR`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotCaldavevents := ParseEvents(tt.args.config, tt.args.events)
|
|
||||||
assert.Equal(t, gotCaldavevents, tt.wantCaldavevents)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseTodos(t *testing.T) {
|
func TestParseTodos(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
config *Config
|
config *Config
|
||||||
@ -520,13 +251,88 @@ X-FUNAMBOL-COLOR:#affffeFF
|
|||||||
CATEGORIES:label1,label2
|
CATEGORIES:label1,label2
|
||||||
LAST-MODIFIED:00010101T000000Z
|
LAST-MODIFIED:00010101T000000Z
|
||||||
END:VTODO
|
END:VTODO
|
||||||
|
END:VCALENDAR`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with alarm",
|
||||||
|
args: args{
|
||||||
|
config: &Config{
|
||||||
|
Name: "test",
|
||||||
|
ProdID: "RandomProdID which is not random",
|
||||||
|
},
|
||||||
|
todos: []*Todo{
|
||||||
|
{
|
||||||
|
Summary: "Todo #1",
|
||||||
|
UID: "randommduid",
|
||||||
|
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
|
Alarms: []Alarm{
|
||||||
|
{
|
||||||
|
Time: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Time: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
|
Description: "alarm description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Duration: -2 * time.Hour,
|
||||||
|
RelativeTo: "due_date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Duration: 1 * time.Hour,
|
||||||
|
RelativeTo: "start_date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Duration: time.Duration(0),
|
||||||
|
RelativeTo: "end_date",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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:20181201T011204Z
|
||||||
|
SUMMARY:Todo #1
|
||||||
|
LAST-MODIFIED:00010101T000000Z
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;VALUE=DATE-TIME:20181201T011204Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Todo #1
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;VALUE=DATE-TIME:20181201T011204Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:alarm description
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;RELATED=END:-PT2H0M0S
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Todo #1
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;RELATED=START:PT1H0M0S
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Todo #1
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;RELATED=END:PT0S
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Todo #1
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
END:VCALENDAR`,
|
END:VCALENDAR`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
gotCaldavtasks := ParseTodos(tt.args.config, tt.args.todos)
|
gotCaldavtasks := ParseTodos(tt.args.config, tt.args.todos)
|
||||||
assert.Equal(t, gotCaldavtasks, tt.wantCaldavtasks)
|
assert.Equal(t, tt.wantCaldavtasks, gotCaldavtasks)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,14 @@
|
|||||||
package caldav
|
package caldav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
|
|
||||||
ics "github.com/arran4/golang-ical"
|
ics "github.com/arran4/golang-ical"
|
||||||
)
|
)
|
||||||
@ -38,6 +40,14 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
|||||||
for _, label := range t.Labels {
|
for _, label := range t.Labels {
|
||||||
categories = append(categories, label.Title)
|
categories = append(categories, label.Title)
|
||||||
}
|
}
|
||||||
|
var alarms []Alarm
|
||||||
|
for _, reminder := range t.Reminders {
|
||||||
|
alarms = append(alarms, Alarm{
|
||||||
|
Time: reminder.Reminder,
|
||||||
|
Duration: time.Duration(reminder.RelativePeriod) * time.Second,
|
||||||
|
RelativeTo: reminder.RelativeTo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
caldavtodos = append(caldavtodos, &Todo{
|
caldavtodos = append(caldavtodos, &Todo{
|
||||||
Timestamp: t.Updated,
|
Timestamp: t.Updated,
|
||||||
@ -56,6 +66,7 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
|||||||
Duration: duration,
|
Duration: duration,
|
||||||
RepeatAfter: t.RepeatAfter,
|
RepeatAfter: t.RepeatAfter,
|
||||||
RepeatMode: t.RepeatMode,
|
RepeatMode: t.RepeatMode,
|
||||||
|
Alarms: alarms,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,10 +83,13 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
vTodo, ok := parsed.Components[0].(*ics.VTodo)
|
||||||
// We put the task details in a map to be able to handle them more easily
|
if !ok {
|
||||||
|
return nil, errors.New("VTODO element not found")
|
||||||
|
}
|
||||||
|
// 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]string)
|
||||||
for _, c := range parsed.Components[0].UnknownPropertiesIANAProperties() {
|
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
|
||||||
task[c.IANAToken] = c.Value
|
task[c.IANAToken] = c.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,9 +141,63 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
|||||||
vTask.EndDate = vTask.StartDate.Add(duration)
|
vTask.EndDate = vTask.StartDate.Add(duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, vAlarm := range vTodo.SubComponents() {
|
||||||
|
if vAlarm, ok := vAlarm.(*ics.VAlarm); ok {
|
||||||
|
vTask = parseVAlarm(vAlarm, vTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) *models.Task {
|
||||||
|
for _, property := range vAlarm.UnknownPropertiesIANAProperties() {
|
||||||
|
if property.IANAToken != "TRIGGER" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if contains(property.ICalParameters["VALUE"], "DATE-TIME") {
|
||||||
|
// Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
||||||
|
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||||
|
Reminder: caldavTimeToTimestamp(property.Value),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := utils.ParseISO8601Duration(property.Value)
|
||||||
|
|
||||||
|
if contains(property.ICalParameters["RELATED"], "END") {
|
||||||
|
// Example: TRIGGER;RELATED=END:-P2D
|
||||||
|
if vTask.EndDate.IsZero() {
|
||||||
|
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||||
|
RelativePeriod: int64(duration.Seconds()),
|
||||||
|
RelativeTo: models.ReminderRelationDueDate})
|
||||||
|
} else {
|
||||||
|
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||||
|
RelativePeriod: int64(duration.Seconds()),
|
||||||
|
RelativeTo: models.ReminderRelationEndDate})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example: TRIGGER;RELATED=START:-P2D
|
||||||
|
// Example: TRIGGER:-PT60M
|
||||||
|
vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{
|
||||||
|
RelativePeriod: int64(duration.Seconds()),
|
||||||
|
RelativeTo: models.ReminderRelationStartDate})
|
||||||
|
}
|
||||||
|
return vTask
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(array []string, str string) bool {
|
||||||
|
for _, value := range array {
|
||||||
|
if value == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 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(tstring string) time.Time {
|
||||||
if tstring == "" {
|
if tstring == "" {
|
||||||
|
@ -118,6 +118,107 @@ END:VCALENDAR`,
|
|||||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "With alarm (time trigger)",
|
||||||
|
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
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`,
|
||||||
|
},
|
||||||
|
wantVTask: &models.Task{
|
||||||
|
Title: "Todo #1",
|
||||||
|
UID: "randomuid",
|
||||||
|
Description: "Lorem Ipsum",
|
||||||
|
Reminders: []*models.TaskReminder{
|
||||||
|
{
|
||||||
|
Reminder: time.Date(2018, 12, 1, 1, 12, 10, 0, config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With alarm (relative trigger)",
|
||||||
|
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
|
||||||
|
DTSTART:20230228T170000Z
|
||||||
|
DUE:20230304T150000Z
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER:PT0S
|
||||||
|
ACTION:DISPLAY
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;VALUE=DURATION:-PT60M
|
||||||
|
ACTION:DISPLAY
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER:-PT61M
|
||||||
|
ACTION:DISPLAY
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;RELATED=START:-P1D
|
||||||
|
ACTION:DISPLAY
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;RELATED=END:-PT30M
|
||||||
|
ACTION:DISPLAY
|
||||||
|
END:VALARM
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR`,
|
||||||
|
},
|
||||||
|
wantVTask: &models.Task{
|
||||||
|
Title: "Todo #1",
|
||||||
|
UID: "randomuid",
|
||||||
|
Description: "Lorem Ipsum",
|
||||||
|
StartDate: time.Date(2023, 2, 28, 17, 0, 0, 0, config.GetTimeZone()),
|
||||||
|
DueDate: time.Date(2023, 3, 4, 15, 0, 0, 0, config.GetTimeZone()),
|
||||||
|
Reminders: []*models.TaskReminder{
|
||||||
|
{
|
||||||
|
RelativeTo: models.ReminderRelationStartDate,
|
||||||
|
RelativePeriod: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RelativeTo: models.ReminderRelationStartDate,
|
||||||
|
RelativePeriod: -3600,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RelativeTo: models.ReminderRelationStartDate,
|
||||||
|
RelativePeriod: -3660,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RelativeTo: models.ReminderRelationStartDate,
|
||||||
|
RelativePeriod: -86400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RelativeTo: models.ReminderRelationDueDate,
|
||||||
|
RelativePeriod: -1800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Updated: time.Unix(1543626724, 0).In(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) {
|
||||||
@ -127,7 +228,7 @@ END:VCALENDAR`,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if diff, equal := messagediff.PrettyDiff(got, tt.wantVTask); !equal {
|
if diff, equal := messagediff.PrettyDiff(got, tt.wantVTask); !equal {
|
||||||
t.Errorf("ParseTaskFromVTODO() gotVTask = %v, want %v, diff = %s", got, tt.wantVTask, diff)
|
t.Errorf("ParseTaskFromVTODO()\n gotVTask = %v\n want %v\n diff = %s", got, tt.wantVTask, diff)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -175,6 +276,16 @@ func TestGetCaldavTodosForTasks(t *testing.T) {
|
|||||||
Title: "label2",
|
Title: "label2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Reminders: []*models.TaskReminder{
|
||||||
|
{
|
||||||
|
Reminder: time.Unix(1543626730, 0).In(config.GetTimeZone()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Reminder: time.Unix(1543626731, 0).In(config.GetTimeZone()),
|
||||||
|
RelativePeriod: -3600,
|
||||||
|
RelativeTo: models.ReminderRelationDueDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -200,6 +311,16 @@ PRIORITY:3
|
|||||||
RRULE:FREQ=SECONDLY;INTERVAL=86400
|
RRULE:FREQ=SECONDLY;INTERVAL=86400
|
||||||
CATEGORIES:label1,label2
|
CATEGORIES:label1,label2
|
||||||
LAST-MODIFIED:20181201T011205Z
|
LAST-MODIFIED:20181201T011205Z
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;VALUE=DATE-TIME:20181201T011210Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Task 1
|
||||||
|
END:VALARM
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;RELATED=END:-PT1H0M0S
|
||||||
|
ACTION:DISPLAY
|
||||||
|
DESCRIPTION:Task 1
|
||||||
|
END:VALARM
|
||||||
END:VTODO
|
END:VTODO
|
||||||
END:VCALENDAR`,
|
END:VCALENDAR`,
|
||||||
},
|
},
|
||||||
|
@ -12,3 +12,7 @@
|
|||||||
task_id: 2
|
task_id: 2
|
||||||
reminder: 2018-12-01 01:13:44
|
reminder: 2018-12-01 01:13:44
|
||||||
created: 2018-12-01 01:12:04
|
created: 2018-12-01 01:12:04
|
||||||
|
- id: 4
|
||||||
|
task_id: 39
|
||||||
|
reminder: 2023-03-04 15:00:00
|
||||||
|
created: 2018-12-01 01:12:04
|
||||||
|
@ -37,6 +37,10 @@ SUMMARY:Caldav Task 1
|
|||||||
CATEGORIES:tag1,tag2,tag3
|
CATEGORIES:tag1,tag2,tag3
|
||||||
CREATED:20230301T073337Z
|
CREATED:20230301T073337Z
|
||||||
LAST-MODIFIED:20230301T073337Z
|
LAST-MODIFIED:20230301T073337Z
|
||||||
|
BEGIN:VALARM
|
||||||
|
TRIGGER;VALUE=DATE-TIME:20230304T150000Z
|
||||||
|
ACTION:DISPLAY
|
||||||
|
END:VALARM
|
||||||
END:VTODO
|
END:VTODO
|
||||||
END:VCALENDAR`
|
END:VCALENDAR`
|
||||||
|
|
||||||
@ -65,5 +69,9 @@ func TestCaldav(t *testing.T) {
|
|||||||
assert.Contains(t, rec.Body.String(), "DUE:20230301T150000Z")
|
assert.Contains(t, rec.Body.String(), "DUE:20230301T150000Z")
|
||||||
assert.Contains(t, rec.Body.String(), "PRIORITY:3")
|
assert.Contains(t, rec.Body.String(), "PRIORITY:3")
|
||||||
assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4")
|
assert.Contains(t, rec.Body.String(), "CATEGORIES:Label #4")
|
||||||
|
assert.Contains(t, rec.Body.String(), "BEGIN:VALARM")
|
||||||
|
assert.Contains(t, rec.Body.String(), "TRIGGER;VALUE=DATE-TIME:20230304T150000Z")
|
||||||
|
assert.Contains(t, rec.Body.String(), "ACTION:DISPLAY")
|
||||||
|
assert.Contains(t, rec.Body.String(), "END:VALARM")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,7 @@ import (
|
|||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -30,6 +28,7 @@ import (
|
|||||||
"code.vikunja.io/api/pkg/models"
|
"code.vikunja.io/api/pkg/models"
|
||||||
"code.vikunja.io/api/pkg/modules/migration"
|
"code.vikunja.io/api/pkg/modules/migration"
|
||||||
"code.vikunja.io/api/pkg/user"
|
"code.vikunja.io/api/pkg/user"
|
||||||
|
"code.vikunja.io/api/pkg/utils"
|
||||||
|
|
||||||
"github.com/gocarina/gocsv"
|
"github.com/gocarina/gocsv"
|
||||||
)
|
)
|
||||||
@ -75,36 +74,6 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copied from https://stackoverflow.com/a/57617885
|
|
||||||
var durationRegex = regexp.MustCompile(`P([\d\.]+Y)?([\d\.]+M)?([\d\.]+D)?T?([\d\.]+H)?([\d\.]+M)?([\d\.]+?S)?`)
|
|
||||||
|
|
||||||
// ParseDuration converts a ISO8601 duration into a time.Duration
|
|
||||||
func parseDuration(str string) time.Duration {
|
|
||||||
matches := durationRegex.FindStringSubmatch(str)
|
|
||||||
|
|
||||||
if len(matches) == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
years := parseDurationPart(matches[1], time.Hour*24*365)
|
|
||||||
months := parseDurationPart(matches[2], time.Hour*24*30)
|
|
||||||
days := parseDurationPart(matches[3], time.Hour*24)
|
|
||||||
hours := parseDurationPart(matches[4], time.Hour)
|
|
||||||
minutes := parseDurationPart(matches[5], time.Second*60)
|
|
||||||
seconds := parseDurationPart(matches[6], time.Second)
|
|
||||||
|
|
||||||
return years + months + days + hours + minutes + seconds
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseDurationPart(value string, unit time.Duration) time.Duration {
|
|
||||||
if len(value) != 0 {
|
|
||||||
if parsed, err := strconv.ParseFloat(value[:len(value)-1], 64); err == nil {
|
|
||||||
return time.Duration(float64(unit) * parsed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.NamespaceWithProjectsAndTasks) {
|
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.NamespaceWithProjectsAndTasks) {
|
||||||
namespace := &models.NamespaceWithProjectsAndTasks{
|
namespace := &models.NamespaceWithProjectsAndTasks{
|
||||||
Namespace: models.Namespace{
|
Namespace: models.Namespace{
|
||||||
@ -231,7 +200,7 @@ func (m *Migrator) Migrate(user *user.User, file io.ReaderAt, size int64) error
|
|||||||
task.IsChecklist = true
|
task.IsChecklist = true
|
||||||
}
|
}
|
||||||
|
|
||||||
reminder := parseDuration(task.ReminderDuration)
|
reminder := utils.ParseISO8601Duration(task.ReminderDuration)
|
||||||
if reminder > 0 {
|
if reminder > 0 {
|
||||||
task.Reminder = reminder
|
task.Reminder = reminder
|
||||||
}
|
}
|
||||||
|
57
pkg/utils/duration.go
Normal file
57
pkg/utils/duration.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// 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 utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseISO8601Duration converts a ISO8601 duration into a time.Duration
|
||||||
|
func ParseISO8601Duration(str string) time.Duration {
|
||||||
|
matches := durationRegex.FindStringSubmatch(str)
|
||||||
|
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
years := parseDurationPart(matches[2], time.Hour*24*365)
|
||||||
|
months := parseDurationPart(matches[3], time.Hour*24*30)
|
||||||
|
days := parseDurationPart(matches[4], time.Hour*24)
|
||||||
|
hours := parseDurationPart(matches[5], time.Hour)
|
||||||
|
minutes := parseDurationPart(matches[6], time.Second*60)
|
||||||
|
seconds := parseDurationPart(matches[7], time.Second)
|
||||||
|
|
||||||
|
duration := years + months + days + hours + minutes + seconds
|
||||||
|
|
||||||
|
if matches[1] == "-" {
|
||||||
|
return -duration
|
||||||
|
}
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationRegex = regexp.MustCompile(`([-+])?P([\d\.]+Y)?([\d\.]+M)?([\d\.]+D)?T?([\d\.]+H)?([\d\.]+M)?([\d\.]+?S)?`)
|
||||||
|
|
||||||
|
func parseDurationPart(value string, unit time.Duration) time.Duration {
|
||||||
|
if len(value) != 0 {
|
||||||
|
if parsed, err := strconv.ParseFloat(value[:len(value)-1], 64); err == nil {
|
||||||
|
return time.Duration(float64(unit) * parsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
39
pkg/utils/duration_test.go
Normal file
39
pkg/utils/duration_test.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 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 utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseISO8601Duration(t *testing.T) {
|
||||||
|
t.Run("full example", func(t *testing.T) {
|
||||||
|
dur := ParseISO8601Duration("P1DT1H1M1S")
|
||||||
|
expected, _ := time.ParseDuration("25h1m1s")
|
||||||
|
|
||||||
|
assert.Equal(t, expected, dur)
|
||||||
|
})
|
||||||
|
t.Run("negative duration", func(t *testing.T) {
|
||||||
|
dur := ParseISO8601Duration("-P1DT1H1M1S")
|
||||||
|
expected, _ := time.ParseDuration("-25h1m1s")
|
||||||
|
|
||||||
|
assert.Equal(t, expected, dur)
|
||||||
|
})
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user