forked from vikunja/vikunja
Compare commits
1 Commits
main
...
renovate/g
Author | SHA1 | Date | |
---|---|---|---|
1b686b834b |
5
go.mod
5
go.mod
|
@ -63,9 +63,9 @@ require (
|
|||
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93
|
||||
github.com/wneessen/go-mail v0.3.7
|
||||
github.com/yuin/goldmark v1.5.3
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/crypto v0.4.0
|
||||
golang.org/x/image v0.3.0
|
||||
golang.org/x/oauth2 v0.3.0
|
||||
golang.org/x/oauth2 v0.4.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.4.0
|
||||
golang.org/x/term v0.4.0
|
||||
|
@ -98,7 +98,6 @@ require (
|
|||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/gocarina/gocsv v0.0.0-20221216233619-1fea7ae8d380 // indirect
|
||||
github.com/goccy/go-json v0.9.11 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -249,8 +249,6 @@ github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
|
|||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWlmOlHkYAb9ZaOMWgU=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA=
|
||||
github.com/gocarina/gocsv v0.0.0-20221216233619-1fea7ae8d380 h1:JJq8YZiS07gFIMYZxkbbiMrXIglG3k5JPPtdvckcnfQ=
|
||||
github.com/gocarina/gocsv v0.0.0-20221216233619-1fea7ae8d380/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
|
||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
|
||||
github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
|
@ -826,8 +824,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -954,6 +950,8 @@ golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7Lm
|
|||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
|
||||
golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
|
||||
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
|
||||
golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
|
@ -27,11 +27,10 @@ import (
|
|||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/migration"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/gocarina/gocsv"
|
||||
)
|
||||
|
||||
const timeISO = "2006-01-02T15:04:05-0700"
|
||||
|
@ -40,39 +39,23 @@ type Migrator struct {
|
|||
}
|
||||
|
||||
type tickTickTask struct {
|
||||
FolderName string `csv:"Folder Name"`
|
||||
ListName string `csv:"List Name"`
|
||||
Title string `csv:"Title"`
|
||||
TagsList string `csv:"Tags"`
|
||||
Tags []string `csv:"-"`
|
||||
Content string `csv:"Content"`
|
||||
IsChecklistString string `csv:"Is Check list"`
|
||||
IsChecklist bool `csv:"-"`
|
||||
StartDate tickTickTime `csv:"Start Date"`
|
||||
DueDate tickTickTime `csv:"Due Date"`
|
||||
ReminderDuration string `csv:"Reminder"`
|
||||
Reminder time.Duration `csv:"-"`
|
||||
Repeat string `csv:"Repeat"`
|
||||
Priority int `csv:"Priority"`
|
||||
Status string `csv:"Status"`
|
||||
CreatedTime tickTickTime `csv:"Created Time"`
|
||||
CompletedTime tickTickTime `csv:"Completed Time"`
|
||||
Order float64 `csv:"Order"`
|
||||
TaskID int64 `csv:"taskId"`
|
||||
ParentID int64 `csv:"parentId"`
|
||||
}
|
||||
|
||||
type tickTickTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
|
||||
date.Time = time.Time{}
|
||||
if csv == "" {
|
||||
return nil
|
||||
}
|
||||
date.Time, err = time.Parse(timeISO, csv)
|
||||
return err
|
||||
FolderName string
|
||||
ListName string
|
||||
Title string
|
||||
Tags []string
|
||||
Content string
|
||||
IsChecklist bool
|
||||
StartDate time.Time
|
||||
DueDate time.Time
|
||||
Reminder time.Duration
|
||||
Repeat string
|
||||
Priority int
|
||||
Status string
|
||||
CreatedTime time.Time
|
||||
CompletedTime time.Time
|
||||
Order float64
|
||||
TaskID int64
|
||||
ParentID int64
|
||||
}
|
||||
|
||||
// Copied from https://stackoverflow.com/a/57617885
|
||||
|
@ -136,22 +119,19 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.Namespace
|
|||
ID: t.TaskID,
|
||||
Title: t.Title,
|
||||
Description: t.Content,
|
||||
StartDate: t.StartDate.Time,
|
||||
EndDate: t.DueDate.Time,
|
||||
DueDate: t.DueDate.Time,
|
||||
Done: t.Status == "1",
|
||||
DoneAt: t.CompletedTime.Time,
|
||||
Position: t.Order,
|
||||
Labels: labels,
|
||||
StartDate: t.StartDate,
|
||||
EndDate: t.DueDate,
|
||||
DueDate: t.DueDate,
|
||||
Reminders: []time.Time{
|
||||
t.DueDate.Add(t.Reminder * -1),
|
||||
},
|
||||
Done: t.Status == "1",
|
||||
DoneAt: t.CompletedTime,
|
||||
Position: t.Order,
|
||||
Labels: labels,
|
||||
},
|
||||
}
|
||||
|
||||
if !t.DueDate.IsZero() && t.Reminder > 0 {
|
||||
task.Task.Reminders = []time.Time{
|
||||
t.DueDate.Add(t.Reminder * -1),
|
||||
}
|
||||
}
|
||||
|
||||
if t.ParentID != 0 {
|
||||
task.RelatedTasks = map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindParenttask: {{ID: t.ParentID}},
|
||||
|
@ -185,22 +165,6 @@ func (m *Migrator) Name() string {
|
|||
return "ticktick"
|
||||
}
|
||||
|
||||
func newLineSkipDecoder(r io.Reader, linesToSkip int) gocsv.SimpleDecoder {
|
||||
reader := csv.NewReader(r)
|
||||
// reader.FieldsPerRecord = -1
|
||||
for i := 0; i < linesToSkip; i++ {
|
||||
_, err := reader.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
log.Debugf("[TickTick Migration] CSV parse error: %s", err)
|
||||
}
|
||||
}
|
||||
reader.FieldsPerRecord = 0
|
||||
return gocsv.NewSimpleDecoderFromCSVReader(reader)
|
||||
}
|
||||
|
||||
// Migrate takes a ticktick export, parses it and imports everything in it into Vikunja.
|
||||
// @Summary Import all lists, tasks etc. from a TickTick backup export
|
||||
// @Description Imports all projects, tasks, notes, reminders, subtasks and files from a TickTick backup export into Vikunja.
|
||||
|
@ -214,26 +178,85 @@ func newLineSkipDecoder(r io.Reader, linesToSkip int) gocsv.SimpleDecoder {
|
|||
// @Router /migration/ticktick/migrate [post]
|
||||
func (m *Migrator) Migrate(user *user.User, file io.ReaderAt, size int64) error {
|
||||
fr := io.NewSectionReader(file, 0, size)
|
||||
//r := csv.NewReader(fr)
|
||||
r := csv.NewReader(fr)
|
||||
|
||||
allTasks := []*tickTickTask{}
|
||||
decode := newLineSkipDecoder(fr, 3)
|
||||
err := gocsv.UnmarshalDecoder(decode, &allTasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
line := 0
|
||||
for {
|
||||
|
||||
for _, task := range allTasks {
|
||||
if task.IsChecklistString == "Y" {
|
||||
task.IsChecklist = true
|
||||
record, err := r.Read()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
log.Debugf("[TickTick Migration] CSV parse error: %s", err)
|
||||
}
|
||||
|
||||
reminder := parseDuration(task.ReminderDuration)
|
||||
if reminder > 0 {
|
||||
task.Reminder = reminder
|
||||
line++
|
||||
if line <= 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
task.Tags = strings.Split(task.TagsList, ", ")
|
||||
priority, err := strconv.Atoi(record[10])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
order, err := strconv.ParseFloat(record[14], 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
taskID, err := strconv.ParseInt(record[21], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentID, err := strconv.ParseInt(record[21], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reminder := parseDuration(record[8])
|
||||
|
||||
t := &tickTickTask{
|
||||
ListName: record[1],
|
||||
Title: record[2],
|
||||
Tags: strings.Split(record[3], ", "),
|
||||
Content: record[4],
|
||||
IsChecklist: record[5] == "Y",
|
||||
Reminder: reminder,
|
||||
Repeat: record[9],
|
||||
Priority: priority,
|
||||
Status: record[11],
|
||||
Order: order,
|
||||
TaskID: taskID,
|
||||
ParentID: parentID,
|
||||
}
|
||||
|
||||
if record[6] != "" {
|
||||
t.StartDate, err = time.Parse(timeISO, record[6])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if record[7] != "" {
|
||||
t.DueDate, err = time.Parse(timeISO, record[7])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if record[12] != "" {
|
||||
t.StartDate, err = time.Parse(timeISO, record[12])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if record[13] != "" {
|
||||
t.CompletedTime, err = time.Parse(timeISO, record[13])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
allTasks = append(allTasks, t)
|
||||
}
|
||||
|
||||
vikunjaTasks := convertTickTickToVikunja(allTasks)
|
||||
|
|
|
@ -26,15 +26,12 @@ import (
|
|||
)
|
||||
|
||||
func TestConvertTicktickTasksToVikunja(t *testing.T) {
|
||||
t1, err := time.Parse(time.RFC3339Nano, "2022-11-18T03:00:00.4770000Z")
|
||||
time1, err := time.Parse(time.RFC3339Nano, "2022-11-18T03:00:00.4770000Z")
|
||||
require.NoError(t, err)
|
||||
time1 := tickTickTime{Time: t1}
|
||||
t2, err := time.Parse(time.RFC3339Nano, "2022-12-18T03:00:00.4770000Z")
|
||||
time2, err := time.Parse(time.RFC3339Nano, "2022-12-18T03:00:00.4770000Z")
|
||||
require.NoError(t, err)
|
||||
time2 := tickTickTime{Time: t2}
|
||||
t3, err := time.Parse(time.RFC3339Nano, "2022-12-10T03:00:00.4770000Z")
|
||||
time3, err := time.Parse(time.RFC3339Nano, "2022-12-10T03:00:00.4770000Z")
|
||||
require.NoError(t, err)
|
||||
time3 := tickTickTime{Time: t3}
|
||||
duration, err := time.ParseDuration("24h")
|
||||
require.NoError(t, err)
|
||||
|
||||
|
@ -94,9 +91,9 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
|
|||
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Title, tickTickTasks[0].Title)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Description, tickTickTasks[0].Content)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].StartDate, tickTickTasks[0].StartDate.Time)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].EndDate, tickTickTasks[0].DueDate.Time)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].DueDate, tickTickTasks[0].DueDate.Time)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].StartDate, tickTickTasks[0].StartDate)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].EndDate, tickTickTasks[0].DueDate)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].DueDate, tickTickTasks[0].DueDate)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Labels, []*models.Label{
|
||||
{Title: "label1"},
|
||||
{Title: "label2"},
|
||||
|
@ -108,7 +105,7 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
|
|||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].Title, tickTickTasks[1].Title)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].Position, tickTickTasks[1].Order)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].Done, true)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].DoneAt, tickTickTasks[1].CompletedTime.Time)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].DoneAt, tickTickTasks[1].CompletedTime)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[1].RelatedTasks, models.RelatedTaskMap{
|
||||
models.RelationKindParenttask: []*models.Task{
|
||||
{
|
||||
|
@ -119,9 +116,9 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) {
|
|||
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Title, tickTickTasks[2].Title)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Description, tickTickTasks[2].Content)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].StartDate, tickTickTasks[2].StartDate.Time)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].EndDate, tickTickTasks[2].DueDate.Time)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].DueDate, tickTickTasks[2].DueDate.Time)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].StartDate, tickTickTasks[2].StartDate)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].EndDate, tickTickTasks[2].DueDate)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].DueDate, tickTickTasks[2].DueDate)
|
||||
assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Labels, []*models.Label{
|
||||
{Title: "label1"},
|
||||
{Title: "label2"},
|
||||
|
|
Loading…
Reference in New Issue
Block a user