diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 5a7b8178b..ead9ebccf 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -26,7 +26,7 @@ import ( "github.com/laurent22/ical-go" ) -func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.Task) string { +func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.TaskWithComments) string { // Make caldav todos from Vikunja todos var caldavtodos []*Todo diff --git a/pkg/models/export.go b/pkg/models/export.go index ff7d8b79e..dd3686975 100644 --- a/pkg/models/export.go +++ b/pkg/models/export.go @@ -18,6 +18,7 @@ package models import ( + "archive/zip" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/cron" "code.vikunja.io/api/pkg/db" @@ -27,8 +28,6 @@ import ( "code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/version" - - "archive/zip" "encoding/json" "fmt" "io" @@ -146,7 +145,7 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err nn.Lists = append(nn.Lists, &ListWithTasksAndBuckets{ List: *l, BackgroundFileID: l.BackgroundFileID, - Tasks: []*Task{}, + Tasks: []*TaskWithComments{}, }) } @@ -181,8 +180,25 @@ func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err err } } + taskMap := make(map[int64]*TaskWithComments, len(tasks)) for _, t := range tasks { - listMap[t.ListID].Tasks = append(listMap[t.ListID].Tasks, t) + taskMap[t.ID] = &TaskWithComments{ + Task: *t, + } + listMap[t.ListID].Tasks = append(listMap[t.ListID].Tasks, taskMap[t.ID]) + } + + comments := []*TaskComment{} + err = s. + Join("LEFT", "tasks", "tasks.id = task_comments.task_id"). + In("tasks.list_id", listIDs). + Find(&comments) + if err != nil { + return + } + + for _, c := range comments { + taskMap[c.TaskID].Comments = append(taskMap[c.TaskID].Comments, c) } buckets := []*Bucket{} diff --git a/pkg/models/list.go b/pkg/models/list.go index 7ae58336d..fc899e0f6 100644 --- a/pkg/models/list.go +++ b/pkg/models/list.go @@ -82,7 +82,7 @@ type List struct { type ListWithTasksAndBuckets struct { List // An array of tasks which belong to the list. - Tasks []*Task `xorm:"-" json:"tasks"` + Tasks []*TaskWithComments `xorm:"-" json:"tasks"` // Only used for migration. Buckets []*Bucket `xorm:"-" json:"buckets"` BackgroundFileID int64 `xorm:"null" json:"background_file_id"` diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 840129ea1..a2e142916 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -129,6 +129,11 @@ type Task struct { web.Rights `xorm:"-" json:"-"` } +type TaskWithComments struct { + Task + Comments []*TaskComment +} + // TableName returns the table name for listtasks func (Task) TableName() string { return "tasks" diff --git a/pkg/modules/migration/create_from_structure.go b/pkg/modules/migration/create_from_structure.go index 6ae47a510..8be8304cf 100644 --- a/pkg/modules/migration/create_from_structure.go +++ b/pkg/modules/migration/create_from_structure.go @@ -129,7 +129,7 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas // Create all tasks for _, t := range tasks { - setBucketOrDefault(t) + setBucketOrDefault(&t.Task) t.ListID = l.ID err = t.Create(s, user) @@ -221,6 +221,15 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas } log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID) } + + for _, comment := range t.Comments { + comment.TaskID = t.ID + err = comment.Create(s, user) + if err != nil { + return + } + log.Debugf("[creating structure] Created new comment %d", comment.ID) + } } // All tasks brought their own bucket with them, therefore the newly created default bucket is just extra space diff --git a/pkg/modules/migration/microsoft-todo/microsoft_todo.go b/pkg/modules/migration/microsoft-todo/microsoft_todo.go index 44a398075..64d20e41a 100644 --- a/pkg/modules/migration/microsoft-todo/microsoft_todo.go +++ b/pkg/modules/migration/microsoft-todo/microsoft_todo.go @@ -342,7 +342,7 @@ func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.Name } } - list.Tasks = append(list.Tasks, task) + list.Tasks = append(list.Tasks, &models.TaskWithComments{Task: *task}) log.Debugf("[Microsoft Todo Migration] Done converted %d tasks", len(l.Tasks)) } diff --git a/pkg/modules/migration/todoist/todoist.go b/pkg/modules/migration/todoist/todoist.go index 76978ab94..e56dfd2e8 100644 --- a/pkg/modules/migration/todoist/todoist.go +++ b/pkg/modules/migration/todoist/todoist.go @@ -264,7 +264,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik lists := make(map[int64]*models.ListWithTasksAndBuckets, len(sync.Projects)) // A map for all vikunja tasks with the todoist task id as key to find them easily and add more data - tasks := make(map[int64]*models.Task, len(sync.Items)) + tasks := make(map[int64]*models.TaskWithComments, len(sync.Items)) // A map for all vikunja labels with the todoist id as key to find them easier labels := make(map[int64]*models.Label, len(sync.Labels)) @@ -307,11 +307,13 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik } for _, i := range sync.Items { - task := &models.Task{ - Title: i.Content, - Created: i.DateAdded.In(config.GetTimeZone()), - Done: i.Checked == 1, - BucketID: i.SectionID, + task := &models.TaskWithComments{ + Task: models.Task{ + Title: i.Content, + Created: i.DateAdded.In(config.GetTimeZone()), + Done: i.Checked == 1, + BucketID: i.SectionID, + }, } // Only try to parse the task done at date if the task is actually done @@ -367,7 +369,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik tasks[i.ParentID].RelatedTasks = make(models.RelatedTaskMap) } - tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask] = append(tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask], tasks[i.ID]) + tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask] = append(tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask], &tasks[i.ID].Task) // Remove the task from the top level structure, otherwise it is added twice outer: diff --git a/pkg/modules/migration/trello/trello.go b/pkg/modules/migration/trello/trello.go index b75fa263e..57e40a3ba 100644 --- a/pkg/modules/migration/trello/trello.go +++ b/pkg/modules/migration/trello/trello.go @@ -271,7 +271,7 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachi log.Debugf("[Trello Migration] Downloaded card attachment %s", attachment.ID) } - list.Tasks = append(list.Tasks, task) + list.Tasks = append(list.Tasks, &models.TaskWithComments{Task: *task}) } list.Buckets = append(list.Buckets, bucket) diff --git a/pkg/modules/migration/vikunja-file/main_test.go b/pkg/modules/migration/vikunja-file/main_test.go new file mode 100644 index 000000000..bd5e11348 --- /dev/null +++ b/pkg/modules/migration/vikunja-file/main_test.go @@ -0,0 +1,44 @@ +// 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 vikunja_file + +import ( + "os" + "testing" + + "code.vikunja.io/api/pkg/events" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/files" + "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/user" +) + +// TestMain is the main test function used to bootstrap the test env +func TestMain(m *testing.M) { + // Set default config + config.InitDefaultConfig() + // We need to set the root path even if we're not using the config, otherwise fixtures are not loaded correctly + config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH")) + + // Some tests use the file engine, so we'll need to initialize that + files.InitTests() + user.InitTests() + models.SetupTests() + events.Fake() + os.Exit(m.Run()) +} diff --git a/pkg/modules/migration/vikunja-file/vikunja.go b/pkg/modules/migration/vikunja-file/vikunja.go index 4f2e451ed..3df306e2d 100644 --- a/pkg/modules/migration/vikunja-file/vikunja.go +++ b/pkg/modules/migration/vikunja-file/vikunja.go @@ -83,12 +83,12 @@ func (v *VikunjaFileMigrator) Migrate(user *user.User, file io.ReaderAt, size in for _, n := range namespaces { for _, l := range n.Lists { for _, t := range l.Tasks { - if len(t.Labels) == 0 { - continue - } for _, label := range t.Labels { label.ID = 0 } + for _, comment := range t.Comments { + comment.ID = 0 + } } } } diff --git a/pkg/modules/migration/vikunja-file/vikunja_test.go b/pkg/modules/migration/vikunja-file/vikunja_test.go new file mode 100644 index 000000000..998646520 --- /dev/null +++ b/pkg/modules/migration/vikunja-file/vikunja_test.go @@ -0,0 +1,78 @@ +// 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 vikunja_file + +import ( + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/user" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestVikunjaFileMigrator_Migrate(t *testing.T) { + db.LoadAndAssertFixtures(t) + + m := &VikunjaFileMigrator{} + u := &user.User{ID: 1} + + f, err := os.Open(config.ServiceRootpath.GetString() + "/pkg/modules/migration/vikunja-file/export.zip") + if err != nil { + t.Fatalf("Could not open file: %s", err) + } + defer f.Close() + s, err := f.Stat() + if err != nil { + t.Fatalf("Could not stat file: %s", err) + } + + err = m.Migrate(u, f, s.Size()) + assert.NoError(t, err) + db.AssertExists(t, "namespaces", map[string]interface{}{ + "title": "test", + "owner_id": u.ID, + }, false) + db.AssertExists(t, "lists", map[string]interface{}{ + "title": "Test list", + "owner_id": u.ID, + }, false) + db.AssertExists(t, "lists", map[string]interface{}{ + "title": "A list with a background", + "owner_id": u.ID, + }, false) + db.AssertExists(t, "tasks", map[string]interface{}{ + "title": "Some other task", + "created_by_id": u.ID, + }, false) + db.AssertExists(t, "task_comments", map[string]interface{}{ + "comment": "This is a comment", + "author_id": u.ID, + }, false) + db.AssertExists(t, "files", map[string]interface{}{ + "name": "cristiano-mozzillo-v3d5uBB26yA-unsplash.jpg", + "created_by_id": u.ID, + }, false) + db.AssertExists(t, "labels", map[string]interface{}{ + "title": "test", + "created_by_id": u.ID, + }, false) + db.AssertExists(t, "buckets", map[string]interface{}{ + "title": "Test Bucket", + "created_by_id": u.ID, + }, false) +} diff --git a/pkg/modules/migration/wunderlist/wunderlist.go b/pkg/modules/migration/wunderlist/wunderlist.go index f57c1d864..fea6722e0 100644 --- a/pkg/modules/migration/wunderlist/wunderlist.go +++ b/pkg/modules/migration/wunderlist/wunderlist.go @@ -235,7 +235,7 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) ( } } - l.Tasks = append(l.Tasks, newTask) + l.Tasks = append(l.Tasks, &models.TaskWithComments{Task: *newTask}) } } return l, nil diff --git a/pkg/routes/caldav/listStorageProvider.go b/pkg/routes/caldav/listStorageProvider.go index 559597d66..0d1692c18 100644 --- a/pkg/routes/caldav/listStorageProvider.go +++ b/pkg/routes/caldav/listStorageProvider.go @@ -174,10 +174,10 @@ func (vcls *VikunjaCaldavListStorage) GetResourcesByFilters(rpath string, filter for _, t := range vcls.list.Tasks { rr := VikunjaListResourceAdapter{ list: vcls.list, - task: t, + task: &t.Task, isCollection: false, } - r := data.NewResource(getTaskURL(t), &rr) + r := data.NewResource(getTaskURL(&t.Task), &rr) r.Name = t.Title resources = append(resources, r) } @@ -371,7 +371,7 @@ func (vcls *VikunjaCaldavListStorage) DeleteResource(rpath string) error { // VikunjaListResourceAdapter holds the actual resource type VikunjaListResourceAdapter struct { list *models.ListWithTasksAndBuckets - listTasks []*models.Task + listTasks []*models.TaskWithComments task *models.Task isPrincipal bool @@ -417,7 +417,7 @@ func (vlra *VikunjaListResourceAdapter) GetContent() string { } if vlra.task != nil { - list := models.ListWithTasksAndBuckets{Tasks: []*models.Task{vlra.task}} + list := models.ListWithTasksAndBuckets{Tasks: []*models.TaskWithComments{{Task: *vlra.task}}} return caldav.GetCaldavTodosForTasks(&list, list.Tasks) } @@ -481,8 +481,10 @@ func (vcls *VikunjaCaldavListStorage) getListRessource(isCollection bool) (rr Vi panic("Tasks returned from TaskCollection.ReadAll are not []*models.Task!") } - listTasks = tasks - vcls.list.Tasks = tasks + for _, t := range tasks { + listTasks = append(listTasks, &models.TaskWithComments{Task: *t}) + } + vcls.list.Tasks = listTasks } if err := s.Commit(); err != nil {