feat(migration): import recurring tasks from todoist
This commit is contained in:
parent
7691f282cf
commit
330b94c3c4
|
|
@ -22,8 +22,10 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
|
|
@ -253,6 +255,72 @@ func parseDate(dateString string) (date time.Time, err error) {
|
|||
return date, err
|
||||
}
|
||||
|
||||
// Matching the existing migration importers, months are treated as 30 days and years as 365.
|
||||
const (
|
||||
secondsPerDay int64 = 60 * 60 * 24
|
||||
secondsPerWeek = secondsPerDay * 7
|
||||
secondsPerMonth = secondsPerDay * 30
|
||||
secondsPerYear = secondsPerDay * 365
|
||||
)
|
||||
|
||||
var repeatUnitSeconds = map[string]int64{
|
||||
"day": secondsPerDay,
|
||||
"week": secondsPerWeek,
|
||||
"month": secondsPerMonth,
|
||||
"year": secondsPerYear,
|
||||
}
|
||||
|
||||
var (
|
||||
todoistRepeatRegex = regexp.MustCompile(`^(?:every\s+)?(?:(\d+)\s+|(other)\s+)?(day|week|month|year)s?$`)
|
||||
todoistRepeatTimeRegex = regexp.MustCompile(`\s+(?:at|@)\s+.*$`)
|
||||
)
|
||||
|
||||
// parseTodoistRepeat translates Todoist's recurrence into a repeat interval in seconds.
|
||||
// Todoist exposes recurrence only as free text (e.g. "every 3 weeks"), so we parse the
|
||||
// common, unambiguous interval phrases. Patterns we can't represent (specific weekdays,
|
||||
// days of the month, non-English strings) return 0, leaving the task non-repeating. Only
|
||||
// the cadence is kept - the due date already anchors the actual day and time.
|
||||
func parseTodoistRepeat(due *dueDate) int64 {
|
||||
if due == nil || !due.IsRecurring {
|
||||
return 0
|
||||
}
|
||||
|
||||
s := strings.ToLower(strings.TrimSpace(due.String))
|
||||
// The time of day is already on the due date, drop it so "every day at 9am" still matches.
|
||||
s = todoistRepeatTimeRegex.ReplaceAllString(s, "")
|
||||
|
||||
switch s {
|
||||
case "daily":
|
||||
return secondsPerDay
|
||||
case "weekly":
|
||||
return secondsPerWeek
|
||||
case "monthly":
|
||||
return secondsPerMonth
|
||||
case "yearly", "annually":
|
||||
return secondsPerYear
|
||||
}
|
||||
|
||||
matches := todoistRepeatRegex.FindStringSubmatch(s)
|
||||
if matches == nil {
|
||||
log.Debugf("[Todoist Migration] Could not parse recurrence %q, leaving task non-repeating", due.String)
|
||||
return 0
|
||||
}
|
||||
|
||||
interval := int64(1)
|
||||
switch {
|
||||
case matches[1] != "":
|
||||
n, err := strconv.ParseInt(matches[1], 10, 64)
|
||||
if err != nil || n < 1 {
|
||||
return 0
|
||||
}
|
||||
interval = n
|
||||
case matches[2] == "other":
|
||||
interval = 2
|
||||
}
|
||||
|
||||
return interval * repeatUnitSeconds[matches[3]]
|
||||
}
|
||||
|
||||
func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
|
||||
|
||||
var pseudoParentID int64 = 1
|
||||
|
|
@ -358,6 +426,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi
|
|||
return nil, err
|
||||
}
|
||||
task.DueDate = dueDate.In(config.GetTimeZone())
|
||||
task.RepeatAfter = parseTodoistRepeat(i.Due)
|
||||
}
|
||||
|
||||
// Put all labels together from earlier
|
||||
|
|
|
|||
|
|
@ -651,3 +651,47 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
t.Errorf("converted todoist data = %v, want %v, diff: %v", hierachie, expectedHierachie, diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTodoistRepeat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
due *dueDate
|
||||
want int64
|
||||
}{
|
||||
{name: "nil due", due: nil, want: 0},
|
||||
{name: "not recurring", due: &dueDate{String: "every day", IsRecurring: false}, want: 0},
|
||||
|
||||
{name: "every day", due: &dueDate{String: "every day", IsRecurring: true}, want: secondsPerDay},
|
||||
{name: "daily", due: &dueDate{String: "daily", IsRecurring: true}, want: secondsPerDay},
|
||||
{name: "every other day", due: &dueDate{String: "every other day", IsRecurring: true}, want: 2 * secondsPerDay},
|
||||
{name: "every 3 days", due: &dueDate{String: "every 3 days", IsRecurring: true}, want: 3 * secondsPerDay},
|
||||
|
||||
{name: "every week", due: &dueDate{String: "every week", IsRecurring: true}, want: secondsPerWeek},
|
||||
{name: "weekly", due: &dueDate{String: "weekly", IsRecurring: true}, want: secondsPerWeek},
|
||||
{name: "every other week", due: &dueDate{String: "every other week", IsRecurring: true}, want: 2 * secondsPerWeek},
|
||||
{name: "every 2 weeks", due: &dueDate{String: "every 2 weeks", IsRecurring: true}, want: 2 * secondsPerWeek},
|
||||
|
||||
{name: "every month", due: &dueDate{String: "every month", IsRecurring: true}, want: secondsPerMonth},
|
||||
{name: "monthly", due: &dueDate{String: "monthly", IsRecurring: true}, want: secondsPerMonth},
|
||||
{name: "every 3 months", due: &dueDate{String: "every 3 months", IsRecurring: true}, want: 3 * secondsPerMonth},
|
||||
|
||||
{name: "every year", due: &dueDate{String: "every year", IsRecurring: true}, want: secondsPerYear},
|
||||
{name: "yearly", due: &dueDate{String: "yearly", IsRecurring: true}, want: secondsPerYear},
|
||||
{name: "annually", due: &dueDate{String: "annually", IsRecurring: true}, want: secondsPerYear},
|
||||
|
||||
{name: "case insensitive", due: &dueDate{String: "Every Day", IsRecurring: true}, want: secondsPerDay},
|
||||
{name: "time of day stripped", due: &dueDate{String: "every day at 9am", IsRecurring: true}, want: secondsPerDay},
|
||||
|
||||
// Tier 1 doesn't understand these, so the task stays non-repeating.
|
||||
{name: "specific weekday", due: &dueDate{String: "every monday", IsRecurring: true}, want: 0},
|
||||
{name: "day of month", due: &dueDate{String: "every 27th", IsRecurring: true}, want: 0},
|
||||
{name: "non-english", due: &dueDate{String: "cada día", IsRecurring: true}, want: 0},
|
||||
{name: "gibberish", due: &dueDate{String: "whenever", IsRecurring: true}, want: 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, parseTodoistRepeat(tt.due))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue