From 3dedc040db8c42eae4112d3aaf64cd1a3f53ef81 Mon Sep 17 00:00:00 2001 From: ce72 Date: Mon, 6 Mar 2023 07:40:13 +0100 Subject: [PATCH 01/24] #1416: Step 1 Rename Reminders to ReminderDates --- pkg/integrations/task_test.go | 6 +-- pkg/models/task_collection_test.go | 8 +-- pkg/models/tasks.go | 52 +++++++++---------- pkg/models/tasks_test.go | 30 +++++------ .../microsoft-todo/microsoft_todo.go | 2 +- .../microsoft-todo/microsoft_todo_test.go | 2 +- pkg/modules/migration/ticktick/ticktick.go | 2 +- .../migration/ticktick/ticktick_test.go | 4 +- pkg/modules/migration/todoist/todoist.go | 2 +- pkg/modules/migration/todoist/todoist_test.go | 10 ++-- 10 files changed, 59 insertions(+), 59 deletions(-) diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go index 1a09f92c9..c8c4f29e5 100644 --- a/pkg/integrations/task_test.go +++ b/pkg/integrations/task_test.go @@ -95,19 +95,19 @@ func TestTask(t *testing.T) { assert.Contains(t, rec.Body.String(), `"due_date":"0001-01-01T00:00:00Z"`) assert.NotContains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`) }) - t.Run("Reminders", func(t *testing.T) { + t.Run("ReminderDates", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminder_dates":["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]`) assert.NotContains(t, rec.Body.String(), `"reminder_dates": null`) }) - t.Run("Reminders unset to empty array", func(t *testing.T) { + t.Run("ReminderDates unset to empty array", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": []}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminder_dates":null`) assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`) }) - t.Run("Reminders unset to null", func(t *testing.T) { + t.Run("ReminderDates unset to null", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": null}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminder_dates":null`) diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 7429ab5af..2e7e0c461 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -169,7 +169,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { label4, }, RelatedTasks: map[RelationKind][]*Task{}, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Unix(1543626824, 0).In(loc), }, Created: time.Unix(1543626724, 0).In(loc), @@ -477,7 +477,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { Index: 12, CreatedByID: 1, CreatedBy: user1, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Unix(1543626724, 0).In(loc), time.Unix(1543626824, 0).In(loc), }, @@ -906,9 +906,9 @@ func TestTaskCollection_ReadAll(t *testing.T) { wantErr: false, }, { - name: "filtered reminders", + name: "filtered reminder dates", fields: fields{ - FilterBy: []string{"reminders", "reminders"}, + FilterBy: []string{"ReminderDates", "ReminderDates"}, FilterValue: []string{"2018-10-01T00:00:00+00:00", "2018-12-10T00:00:00+00:00"}, FilterComparator: []string{"greater", "less"}, }, diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 0c79961cb..4af5429c3 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -62,7 +62,7 @@ type Task struct { // The time when the task is due. DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"` // An array of datetimes when the user wants to be reminded of the task. - Reminders []time.Time `xorm:"-" json:"reminder_dates"` + ReminderDates []time.Time `xorm:"-" json:"reminder_dates"` // The list this task belongs to. ListID int64 `xorm:"bigint INDEX not null" json:"list_id" param:"list"` // An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount. @@ -334,7 +334,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO var filters = make([]builder.Cond, 0, len(opts.filters)) // To still find tasks with nil values, we exclude 0s when comparing with >/< values. for _, f := range opts.filters { - if f.field == "reminders" { + if f.field == "ReminderDates" { f.field = "reminder" // This is the name in the db filter, err := getFilterCond(f, opts.filterIncludeNulls) if err != nil { @@ -773,7 +773,7 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e task.CreatedBy = users[task.CreatedByID] // Add the reminders - task.Reminders = taskReminders[task.ID] + task.ReminderDates = taskReminders[task.ID] // Prepare the subtasks task.RelatedTasks = make(RelatedTaskMap) @@ -965,7 +965,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err } // Update the reminders - if err := t.updateReminders(s, t.Reminders); err != nil { + if err := t.updateReminders(s, t.ReminderDates); err != nil { return err } @@ -1023,9 +1023,9 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { return } - ot.Reminders = make([]time.Time, len(reminders)) + ot.ReminderDates = make([]time.Time, len(reminders)) for i, r := range reminders { - ot.Reminders[i] = r.Reminder + ot.ReminderDates[i] = r.Reminder } targetBucket, err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID) @@ -1049,7 +1049,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { } // Update the reminders - if err := ot.updateReminders(s, t.Reminders); err != nil { + if err := ot.updateReminders(s, t.ReminderDates); err != nil { return err } @@ -1326,14 +1326,14 @@ func setTaskDatesDefault(oldTask, newTask *Task) { } } - newTask.Reminders = oldTask.Reminders + newTask.ReminderDates = oldTask.ReminderDates // When repeating from the current date, all reminders should keep their difference to each other. // To make this easier, we sort them first because we can then rely on the fact the first is the smallest - if len(oldTask.Reminders) > 0 { - for in, r := range oldTask.Reminders { - newTask.Reminders[in] = r.Add(repeatDuration) - for !newTask.Reminders[in].After(now) { - newTask.Reminders[in] = newTask.Reminders[in].Add(repeatDuration) + if len(oldTask.ReminderDates) > 0 { + for in, r := range oldTask.ReminderDates { + newTask.ReminderDates[in] = r.Add(repeatDuration) + for !newTask.ReminderDates[in].After(now) { + newTask.ReminderDates[in] = newTask.ReminderDates[in].Add(repeatDuration) } } } @@ -1361,10 +1361,10 @@ func setTaskDatesMonthRepeat(oldTask, newTask *Task) { newTask.DueDate = addOneMonthToDate(oldTask.DueDate) } - newTask.Reminders = oldTask.Reminders - if len(oldTask.Reminders) > 0 { - for in, r := range oldTask.Reminders { - newTask.Reminders[in] = addOneMonthToDate(r) + newTask.ReminderDates = oldTask.ReminderDates + if len(oldTask.ReminderDates) > 0 { + for in, r := range oldTask.ReminderDates { + newTask.ReminderDates[in] = addOneMonthToDate(r) } } @@ -1400,17 +1400,17 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) { newTask.DueDate = now.Add(repeatDuration) } - newTask.Reminders = oldTask.Reminders + newTask.ReminderDates = oldTask.ReminderDates // When repeating from the current date, all reminders should keep their difference to each other. // To make this easier, we sort them first because we can then rely on the fact the first is the smallest - if len(oldTask.Reminders) > 0 { - sort.Slice(oldTask.Reminders, func(i, j int) bool { - return oldTask.Reminders[i].Unix() < oldTask.Reminders[j].Unix() + if len(oldTask.ReminderDates) > 0 { + sort.Slice(oldTask.ReminderDates, func(i, j int) bool { + return oldTask.ReminderDates[i].Unix() < oldTask.ReminderDates[j].Unix() }) - first := oldTask.Reminders[0] - for in, r := range oldTask.Reminders { + first := oldTask.ReminderDates[0] + for in, r := range oldTask.ReminderDates { diff := r.Sub(first) - newTask.Reminders[in] = now.Add(repeatDuration + diff) + newTask.ReminderDates[in] = now.Add(repeatDuration + diff) } } @@ -1505,9 +1505,9 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro } } - t.Reminders = reminders + t.ReminderDates = reminders if len(reminders) == 0 { - t.Reminders = nil + t.ReminderDates = nil } err = updateListLastUpdated(s, &List{ID: t.ListID}) diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index a6bbc71c4..fc028520c 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -376,7 +376,7 @@ func TestTask_Update(t *testing.T) { task := &Task{ ID: 1, Title: "test", - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Unix(1674745156, 0), time.Unix(1674745156, 223), }, @@ -487,7 +487,7 @@ func TestUpdateDone(t *testing.T) { oldTask := &Task{ Done: false, RepeatAfter: 8600, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Unix(1550000000, 0), time.Unix(1555000000, 0), }, @@ -506,9 +506,9 @@ func TestUpdateDone(t *testing.T) { expected2 = expected2.Add(time.Duration(oldTask.RepeatAfter) * time.Second) } - assert.Len(t, newTask.Reminders, 2) - assert.Equal(t, expected1, newTask.Reminders[0]) - assert.Equal(t, expected2, newTask.Reminders[1]) + assert.Len(t, newTask.ReminderDates, 2) + assert.Equal(t, expected1, newTask.ReminderDates[0]) + assert.Equal(t, expected2, newTask.ReminderDates[1]) assert.False(t, newTask.Done) }) t.Run("update start date", func(t *testing.T) { @@ -585,7 +585,7 @@ func TestUpdateDone(t *testing.T) { Done: false, RepeatAfter: 8600, RepeatMode: TaskRepeatModeFromCurrentDate, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Unix(1550000000, 0), time.Unix(1555000000, 0), }, @@ -595,12 +595,12 @@ func TestUpdateDone(t *testing.T) { } updateDone(oldTask, newTask) - diff := oldTask.Reminders[1].Sub(oldTask.Reminders[0]) + diff := oldTask.ReminderDates[1].Sub(oldTask.ReminderDates[0]) - assert.Len(t, newTask.Reminders, 2) + assert.Len(t, newTask.ReminderDates, 2) // Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value - assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[0].Unix()) - assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[1].Unix()) + assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.ReminderDates[0].Unix()) + assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.ReminderDates[1].Unix()) assert.False(t, newTask.Done) }) t.Run("start date", func(t *testing.T) { @@ -678,7 +678,7 @@ func TestUpdateDone(t *testing.T) { oldTask := &Task{ Done: false, RepeatMode: TaskRepeatModeMonth, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Unix(1550000000, 0), time.Unix(1555000000, 0), }, @@ -686,13 +686,13 @@ func TestUpdateDone(t *testing.T) { newTask := &Task{ Done: true, } - oldReminders := make([]time.Time, len(oldTask.Reminders)) - copy(oldReminders, oldTask.Reminders) + oldReminders := make([]time.Time, len(oldTask.ReminderDates)) + copy(oldReminders, oldTask.ReminderDates) updateDone(oldTask, newTask) - assert.Len(t, newTask.Reminders, len(oldReminders)) - for i, r := range newTask.Reminders { + assert.Len(t, newTask.ReminderDates, len(oldReminders)) + for i, r := range newTask.ReminderDates { assert.True(t, r.After(oldReminders[i])) assert.NotEqual(t, oldReminders[i].Month(), r.Month()) } diff --git a/pkg/modules/migration/microsoft-todo/microsoft_todo.go b/pkg/modules/migration/microsoft-todo/microsoft_todo.go index 0cf390f7f..e2fd5a1d8 100644 --- a/pkg/modules/migration/microsoft-todo/microsoft_todo.go +++ b/pkg/modules/migration/microsoft-todo/microsoft_todo.go @@ -329,7 +329,7 @@ func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.Name return nil, err } - task.Reminders = []time.Time{reminder} + task.ReminderDates = []time.Time{reminder} } // Due Date diff --git a/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go b/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go index a99511567..7c3a66333 100644 --- a/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go +++ b/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go @@ -141,7 +141,7 @@ func TestConverting(t *testing.T) { { Task: models.Task{ Title: "Task 5", - Reminders: []time.Time{ + ReminderDates: []time.Time{ testtimeTime, }, }, diff --git a/pkg/modules/migration/ticktick/ticktick.go b/pkg/modules/migration/ticktick/ticktick.go index b553b567b..0870ae87d 100644 --- a/pkg/modules/migration/ticktick/ticktick.go +++ b/pkg/modules/migration/ticktick/ticktick.go @@ -147,7 +147,7 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.Namespace } if !t.DueDate.IsZero() && t.Reminder > 0 { - task.Task.Reminders = []time.Time{ + task.Task.ReminderDates = []time.Time{ t.DueDate.Add(t.Reminder * -1), } } diff --git a/pkg/modules/migration/ticktick/ticktick_test.go b/pkg/modules/migration/ticktick/ticktick_test.go index a6834442e..1638b3769 100644 --- a/pkg/modules/migration/ticktick/ticktick_test.go +++ b/pkg/modules/migration/ticktick/ticktick_test.go @@ -101,7 +101,7 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) { {Title: "label1"}, {Title: "label2"}, }) - //assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO + //assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].ReminderDates, tickTickTasks[0].) // TODO assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Position, tickTickTasks[0].Order) assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Done, false) @@ -127,7 +127,7 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) { {Title: "label2"}, {Title: "other label"}, }) - //assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Reminders, tickTickTasks[0].) // TODO + //assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].ReminderDates, tickTickTasks[0].) // TODO assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Position, tickTickTasks[2].Order) assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Done, false) diff --git a/pkg/modules/migration/todoist/todoist.go b/pkg/modules/migration/todoist/todoist.go index 269b5d3de..c0fce8231 100644 --- a/pkg/modules/migration/todoist/todoist.go +++ b/pkg/modules/migration/todoist/todoist.go @@ -471,7 +471,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi return nil, err } - tasks[r.ItemID].Reminders = append(tasks[r.ItemID].Reminders, date.In(config.GetTimeZone())) + tasks[r.ItemID].ReminderDates = append(tasks[r.ItemID].ReminderDates, date.In(config.GetTimeZone())) } return []*models.NamespaceWithListsAndTasks{ diff --git a/pkg/modules/migration/todoist/todoist_test.go b/pkg/modules/migration/todoist/todoist_test.go index d75a0a469..dd697d47f 100644 --- a/pkg/modules/migration/todoist/todoist_test.go +++ b/pkg/modules/migration/todoist/todoist_test.go @@ -388,7 +388,7 @@ func TestConvertTodoistToVikunja(t *testing.T) { Description: "Lorem Ipsum dolor sit amet", Done: false, Created: time1, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Date(2020, time.June, 15, 23, 59, 0, 0, time.UTC).In(config.GetTimeZone()), time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), }, @@ -407,7 +407,7 @@ func TestConvertTodoistToVikunja(t *testing.T) { Title: "Task400000002", Done: false, Created: time1, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), }, }, @@ -421,7 +421,7 @@ func TestConvertTodoistToVikunja(t *testing.T) { Created: time1, DoneAt: time3, Labels: vikunjaLabels, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), }, }, @@ -441,7 +441,7 @@ func TestConvertTodoistToVikunja(t *testing.T) { DueDate: dueTime, Created: time1, DoneAt: time3, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), }, }, @@ -531,7 +531,7 @@ func TestConvertTodoistToVikunja(t *testing.T) { Title: "Task400000009", Done: false, Created: time1, - Reminders: []time.Time{ + ReminderDates: []time.Time{ time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), }, }, -- 2.45.1 From 85267d26bba8fd876f23498e8eed539412b6f208 Mon Sep 17 00:00:00 2001 From: ce72 Date: Mon, 6 Mar 2023 22:06:20 +0100 Subject: [PATCH 02/24] #1416: Step 2 Read and write Task.Reminders --- pkg/integrations/task_collection_test.go | 46 ++++++++-------- pkg/integrations/task_test.go | 23 ++++++++ pkg/models/task_collection_test.go | 22 ++++++++ pkg/models/task_reminder.go | 7 +-- pkg/models/tasks.go | 68 ++++++++++++++++++++---- pkg/models/tasks_test.go | 33 +++++++++++- pkg/swagger/docs.go | 30 +++++++++-- pkg/swagger/swagger.json | 27 +++++++++- pkg/swagger/swagger.yaml | 28 ++++++++-- 9 files changed, 235 insertions(+), 49 deletions(-) diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go index 93ba86df0..5dbb36991 100644 --- a/pkg/integrations/task_collection_test.go +++ b/pkg/integrations/task_collection_test.go @@ -113,49 +113,49 @@ func TestTaskCollection(t *testing.T) { t.Run("by priority", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by priority desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) // should equal duedate asc t.Run("by due_date", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) + assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) }) // Due date without unix suffix t.Run("by duedate asc without suffix", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) + assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) }) t.Run("by due_date without suffix", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) + assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) }) t.Run("by duedate desc without suffix", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) }) t.Run("by duedate asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) + assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) }) t.Run("invalid sort parameter", func(t *testing.T) { _, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams) @@ -171,10 +171,10 @@ func TestTaskCollection(t *testing.T) { // Invalid parameter should not sort at all rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, urlParams) assert.NoError(t, err) - assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`) - assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) - assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`) - assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) + assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`) + assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) + assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`) + assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) }) }) t.Run("Filter", func(t *testing.T) { @@ -366,42 +366,42 @@ func TestTaskCollection(t *testing.T) { t.Run("by priority", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) t.Run("by priority desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":4,"title":"task #4 low prio","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":1,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`) }) // should equal duedate asc t.Run("by due_date", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) + assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`) }) t.Run("by duedate asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) + assert.Contains(t, rec.Body.String(), `[{"id":6,"title":"task #6 lower due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-11-30T22:25:24Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":3,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminder_dates":null,"reminders":null,"list_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":2,"position":0,"kanban_position":0,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}`) }) t.Run("invalid parameter", func(t *testing.T) { // Invalid parameter should not sort at all rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, nil) assert.NoError(t, err) - assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`) - assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) - assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`) - assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) + assert.NotContains(t, rec.Body.String(), `[{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":1`) + assert.NotContains(t, rec.Body.String(), `{"id":4,"title":"task #4 low prio","description":"","done":false,"due_date":0,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":1,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":3,"title":"task #3 high prio","description":"","done":false,"due_date":0,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":100,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) + assert.NotContains(t, rec.Body.String(), `[{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":6,"title":"task #6 lower due date"`) + assert.NotContains(t, rec.Body.String(), `{"id":6,"title":"task #6 lower due date","description":"","done":false,"due_date":1543616724,"reminder_dates":null,"reminders":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"hex_color":"","created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"due_date":1543636724,"reminder_dates":null,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":0,"end_date":0,"assignees":null,"labels":null,"created":1543626724,"updated":1543626724,"created_by":{"id":0,"name":"","username":"","email":"","created":0,"updated":0}}]`) }) }) t.Run("Filter", func(t *testing.T) { diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go index c8c4f29e5..f2cc3eb97 100644 --- a/pkg/integrations/task_test.go +++ b/pkg/integrations/task_test.go @@ -95,24 +95,47 @@ func TestTask(t *testing.T) { assert.Contains(t, rec.Body.String(), `"due_date":"0001-01-01T00:00:00Z"`) assert.NotContains(t, rec.Body.String(), `"due_date":"2020-02-10T10:00:00Z"`) }) + // Deprecated: Remove if ReminderDates is removed t.Run("ReminderDates", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminder_dates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminder_dates":["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]`) assert.NotContains(t, rec.Body.String(), `"reminder_dates": null`) }) + // Deprecated: Remove if ReminderDates is removed t.Run("ReminderDates unset to empty array", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": []}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminder_dates":null`) assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`) }) + // Deprecated: Remove if ReminderDates is removed t.Run("ReminderDates unset to null", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminder_dates": null}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminder_dates":null`) assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`) }) + t.Run("Reminders", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminders": [{"Reminder": "2020-02-10T10:00:00Z"},{"Reminder": "2020-02-11T10:00:00Z"}]}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"reminders":[`) + assert.Contains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"}`) + assert.Contains(t, rec.Body.String(), `{"Reminder":"2020-02-11T10:00:00Z"}`) + assert.NotContains(t, rec.Body.String(), `"reminders":null`) + }) + t.Run("Reminders unset to empty array", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminders": []}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"reminders":null`) + assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"}`) + }) + t.Run("Reminders unset to null", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminders": null}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"reminder_dates":null`) + assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"}`) + }) t.Run("Repeat after", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"repeat_after":3600}`) assert.NoError(t, err) diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 2e7e0c461..e8a65e27f 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -172,6 +172,14 @@ func TestTaskCollection_ReadAll(t *testing.T) { ReminderDates: []time.Time{ time.Unix(1543626824, 0).In(loc), }, + Reminders: []*TaskReminder{ + { + ID: 3, + TaskID: 2, + Reminder: time.Unix(1543626824, 0).In(loc), + Created: time.Unix(1543626724, 0).In(loc), + }, + }, Created: time.Unix(1543626724, 0).In(loc), Updated: time.Unix(1543626724, 0).In(loc), } @@ -481,6 +489,20 @@ func TestTaskCollection_ReadAll(t *testing.T) { time.Unix(1543626724, 0).In(loc), time.Unix(1543626824, 0).In(loc), }, + Reminders: []*TaskReminder{ + { + ID: 1, + TaskID: 27, + Reminder: time.Unix(1543626724, 0).In(loc), + Created: time.Unix(1543626724, 0).In(loc), + }, + { + ID: 2, + TaskID: 27, + Reminder: time.Unix(1543626824, 0).In(loc), + Created: time.Unix(1543626724, 0).In(loc), + }, + }, ListID: 1, BucketID: 1, RelatedTasks: map[RelationKind][]*Task{}, diff --git a/pkg/models/task_reminder.go b/pkg/models/task_reminder.go index 42b337b6c..56d7180bb 100644 --- a/pkg/models/task_reminder.go +++ b/pkg/models/task_reminder.go @@ -35,10 +35,11 @@ import ( // TaskReminder holds a reminder on a task type TaskReminder struct { - ID int64 `xorm:"bigint autoincr not null unique pk"` - TaskID int64 `xorm:"bigint not null INDEX"` + ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"` + TaskID int64 `xorm:"bigint not null INDEX" json:"-"` + // The absolute time when the user wants to be reminded of the task. Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'"` - Created time.Time `xorm:"created not null"` + Created time.Time `xorm:"created not null" json:"-"` } // TableName returns a pretty table name diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 4af5429c3..26ef24cc9 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -62,7 +62,11 @@ type Task struct { // The time when the task is due. DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"` // An array of datetimes when the user wants to be reminded of the task. + // + // Deprecated: Use Reminders[] ReminderDates []time.Time `xorm:"-" json:"reminder_dates"` + // An array of reminders that are associated with this task. + Reminders []*TaskReminder `xorm:"-" json:"reminders"` // The list this task belongs to. ListID int64 `xorm:"bigint INDEX not null" json:"list_id" param:"list"` // An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount. @@ -641,8 +645,8 @@ func addAttachmentsToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]* return } -func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int64][]time.Time, err error) { - taskReminders = make(map[int64][]time.Time) +func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int64][]*TaskReminder, err error) { + taskReminders = make(map[int64][]*TaskReminder) // Get all reminders and put them in a map to have it easier later reminders, err := getRemindersForTasks(s, taskIDs) @@ -651,7 +655,7 @@ func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int } for _, r := range reminders { - taskReminders[r.TaskID] = append(taskReminders[r.TaskID], r.Reminder) + taskReminders[r.TaskID] = append(taskReminders[r.TaskID], r) } return @@ -772,8 +776,13 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (e // Make created by user objects task.CreatedBy = users[task.CreatedByID] + // Add the reminder dates (Remove, when ReminderDates is removed) + for _, r := range taskReminders[task.ID] { + task.ReminderDates = append(task.ReminderDates, r.Reminder) + } + // Add the reminders - task.ReminderDates = taskReminders[task.ID] + task.Reminders = taskReminders[task.ID] // Prepare the subtasks task.RelatedTasks = make(RelatedTaskMap) @@ -965,7 +974,14 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err } // Update the reminders - if err := t.updateReminders(s, t.ReminderDates); err != nil { + // + // Deprecated: the if clause can be removed if ReminderDates will be removed + if t.ReminderDates != nil { + if err := t.updateRemindersFromReminderDates(t.ReminderDates); err != nil { + return err + } + } + if err := t.updateReminders(s, t.Reminders); err != nil { return err } @@ -1023,6 +1039,10 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { return } + // Old task has the stored reminders + ot.Reminders = reminders + + // Deprecated: remove when ReminderDates is removed ot.ReminderDates = make([]time.Time, len(reminders)) for i, r := range reminders { ot.ReminderDates[i] = r.Reminder @@ -1048,8 +1068,14 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { return err } + // Deprecated: This statement can be removed if ReminderDates will be removed + if t.ReminderDates != nil { + if err := t.updateRemindersFromReminderDates(t.ReminderDates); err != nil { + return err + } + } // Update the reminders - if err := ot.updateReminders(s, t.ReminderDates); err != nil { + if err := ot.updateReminders(s, t.Reminders); err != nil { return err } @@ -1478,11 +1504,26 @@ func updateDone(oldTask *Task, newTask *Task) { } } +// Deprecated: will be removed when ReminderDates are removed from Task. +// For now the method just creates Taskeminder Objects from the ReminderDates +func (t *Task) updateRemindersFromReminderDates(reminders []time.Time) (err error) { + + for _, reminder := range reminders { + t.Reminders = append(t.Reminders, &TaskReminder{TaskID: t.ID, Reminder: reminder}) + } + + t.ReminderDates = reminders + if len(reminders) == 0 { + t.ReminderDates = nil + } + return +} + // Removes all old reminders and adds the new ones. This is a lot easier and less buggy than // trying to figure out which reminders changed and then only re-add those needed. And since it does // not make a performance difference we'll just do that. // The parameter is a slice with unix dates which holds the new reminders. -func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err error) { +func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder) (err error) { _, err = s. Where("task_id = ?", t.ID). @@ -1492,21 +1533,26 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []time.Time) (err erro } // Resolve duplicates and sort them - reminderMap := make(map[int64]time.Time, len(reminders)) + reminderMap := make(map[int64]TaskReminder, len(reminders)) for _, reminder := range reminders { - reminderMap[reminder.UTC().Unix()] = reminder + reminderMap[reminder.Reminder.UTC().Unix()] = *reminder } + t.Reminders = make([]*TaskReminder, 0, len(reminderMap)) + t.ReminderDates = make([]time.Time, 0, len(reminderMap)) + // Loop through all reminders and add them for _, r := range reminderMap { - _, err = s.Insert(&TaskReminder{TaskID: t.ID, Reminder: r}) + _, err = s.Insert(&TaskReminder{TaskID: t.ID, Reminder: r.Reminder}) + t.Reminders = append(t.Reminders, &r) + t.ReminderDates = append(t.ReminderDates, r.Reminder) if err != nil { return err } } - t.ReminderDates = reminders if len(reminders) == 0 { + t.Reminders = nil t.ReminderDates = nil } diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index fc028520c..835789b14 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -98,7 +98,7 @@ func TestTask_Create(t *testing.T) { assert.Error(t, err) assert.True(t, IsErrListDoesNotExist(err)) }) - t.Run("noneixtant user", func(t *testing.T) { + t.Run("nonexistant user", func(t *testing.T) { db.LoadAndAssertFixtures(t) s := db.NewSession() defer s.Close() @@ -368,7 +368,9 @@ func TestTask_Update(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(3), task.Index) }) - t.Run("the same date multiple times should be saved once", func(t *testing.T) { + + // Deprecated: see next test + t.Run("the same reminder date multiple times should be saved once", func(t *testing.T) { db.LoadAndAssertFixtures(t) s := db.NewSession() defer s.Close() @@ -389,6 +391,33 @@ func TestTask_Update(t *testing.T) { db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 1) }) + + t.Run("the same reminder multiple times should be saved once", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + taskReminders := []*TaskReminder{ + { + Reminder: time.Unix(1674745156, 0), + }, + { + Reminder: time.Unix(1674745156, 223), + }, + } + + task := &Task{ + ID: 1, + Title: "test", + Reminders: taskReminders, + ListID: 1, + } + err := task.Update(s, u) + assert.NoError(t, err) + err = s.Commit() + assert.NoError(t, err) + db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 1) + }) } func TestTask_Delete(t *testing.T) { diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 90b59f570..dd934ae38 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,5 +1,4 @@ -// Package swagger GENERATED BY SWAG; DO NOT EDIT -// This file was generated by swaggo/swag +// Code generated by swaggo/swag. DO NOT EDIT package swagger import "github.com/swaggo/swag" @@ -7791,12 +7790,19 @@ const docTemplate = `{ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", "type": "array", "items": { "type": "string" } }, + "reminders": { + "description": "An array of reminders that are associated with this task.", + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskReminder" + } + }, "repeat_after": { "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", "type": "integer" @@ -8518,12 +8524,19 @@ const docTemplate = `{ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", "type": "array", "items": { "type": "string" } }, + "reminders": { + "description": "An array of reminders that are associated with this task.", + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskReminder" + } + }, "repeat_after": { "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", "type": "integer" @@ -8691,6 +8704,15 @@ const docTemplate = `{ } } }, + "models.TaskReminder": { + "type": "object", + "properties": { + "reminder": { + "description": "The absolute time when the user wants to be reminded of the task.", + "type": "string" + } + } + }, "models.TaskRepeatMode": { "type": "integer", "enum": [ diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index bf9681b55..00ac515b7 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7782,12 +7782,19 @@ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", "type": "array", "items": { "type": "string" } }, + "reminders": { + "description": "An array of reminders that are associated with this task.", + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskReminder" + } + }, "repeat_after": { "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", "type": "integer" @@ -8509,12 +8516,19 @@ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", "type": "array", "items": { "type": "string" } }, + "reminders": { + "description": "An array of reminders that are associated with this task.", + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskReminder" + } + }, "repeat_after": { "description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.", "type": "integer" @@ -8682,6 +8696,15 @@ } } }, + "models.TaskReminder": { + "type": "object", + "properties": { + "reminder": { + "description": "The absolute time when the user wants to be reminded of the task.", + "type": "string" + } + } + }, "models.TaskRepeatMode": { "type": "integer", "enum": [ diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 393d2054b..9811d4ca2 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -196,11 +196,18 @@ definitions: - $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: - description: An array of datetimes when the user wants to be reminded of the - task. + description: |- + An array of datetimes when the user wants to be reminded of the task. + + Deprecated: Use Reminders[] items: type: string type: array + reminders: + description: An array of reminders that are associated with this task. + items: + $ref: '#/definitions/models.TaskReminder' + type: array repeat_after: description: An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then @@ -762,11 +769,18 @@ definitions: - $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: - description: An array of datetimes when the user wants to be reminded of the - task. + description: |- + An array of datetimes when the user wants to be reminded of the task. + + Deprecated: Use Reminders[] items: type: string type: array + reminders: + description: An array of reminders that are associated with this task. + items: + $ref: '#/definitions/models.TaskReminder' + type: array repeat_after: description: An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then @@ -888,6 +902,12 @@ definitions: description: The ID of the "base" task, the task which has a relation to another. type: integer type: object + models.TaskReminder: + properties: + reminder: + description: The absolute time when the user wants to be reminded of the task. + type: string + type: object models.TaskRepeatMode: enum: - 0 -- 2.45.1 From 4a41a8a6ca531dc51857928ee0e9a6ca3c89f164 Mon Sep 17 00:00:00 2001 From: ce72 Date: Tue, 7 Mar 2023 08:27:56 +0100 Subject: [PATCH 03/24] #1416: Step 2b Use Reminders in updateDone and migrations --- pkg/models/tasks.go | 43 +++++++------ pkg/models/tasks_test.go | 64 +++++++++++-------- .../microsoft-todo/microsoft_todo.go | 4 +- .../microsoft-todo/microsoft_todo_test.go | 6 +- pkg/modules/migration/todoist/todoist.go | 5 +- pkg/modules/migration/todoist/todoist_test.go | 22 +++---- 6 files changed, 82 insertions(+), 62 deletions(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 26ef24cc9..fdb6f8492 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1352,14 +1352,14 @@ func setTaskDatesDefault(oldTask, newTask *Task) { } } - newTask.ReminderDates = oldTask.ReminderDates + newTask.Reminders = oldTask.Reminders // When repeating from the current date, all reminders should keep their difference to each other. // To make this easier, we sort them first because we can then rely on the fact the first is the smallest - if len(oldTask.ReminderDates) > 0 { - for in, r := range oldTask.ReminderDates { - newTask.ReminderDates[in] = r.Add(repeatDuration) - for !newTask.ReminderDates[in].After(now) { - newTask.ReminderDates[in] = newTask.ReminderDates[in].Add(repeatDuration) + if len(oldTask.Reminders) > 0 { + for in, r := range oldTask.Reminders { + newTask.Reminders[in].Reminder = r.Reminder.Add(repeatDuration) + for !newTask.Reminders[in].Reminder.After(now) { + newTask.Reminders[in].Reminder = newTask.Reminders[in].Reminder.Add(repeatDuration) } } } @@ -1387,10 +1387,10 @@ func setTaskDatesMonthRepeat(oldTask, newTask *Task) { newTask.DueDate = addOneMonthToDate(oldTask.DueDate) } - newTask.ReminderDates = oldTask.ReminderDates - if len(oldTask.ReminderDates) > 0 { - for in, r := range oldTask.ReminderDates { - newTask.ReminderDates[in] = addOneMonthToDate(r) + newTask.Reminders = oldTask.Reminders + if len(oldTask.Reminders) > 0 { + for in, r := range oldTask.Reminders { + newTask.Reminders[in].Reminder = addOneMonthToDate(r.Reminder) } } @@ -1426,17 +1426,17 @@ func setTaskDatesFromCurrentDateRepeat(oldTask, newTask *Task) { newTask.DueDate = now.Add(repeatDuration) } - newTask.ReminderDates = oldTask.ReminderDates + newTask.Reminders = oldTask.Reminders // When repeating from the current date, all reminders should keep their difference to each other. // To make this easier, we sort them first because we can then rely on the fact the first is the smallest - if len(oldTask.ReminderDates) > 0 { - sort.Slice(oldTask.ReminderDates, func(i, j int) bool { - return oldTask.ReminderDates[i].Unix() < oldTask.ReminderDates[j].Unix() + if len(oldTask.Reminders) > 0 { + sort.Slice(oldTask.Reminders, func(i, j int) bool { + return oldTask.Reminders[i].Reminder.Unix() < oldTask.Reminders[j].Reminder.Unix() }) - first := oldTask.ReminderDates[0] - for in, r := range oldTask.ReminderDates { - diff := r.Sub(first) - newTask.ReminderDates[in] = now.Add(repeatDuration + diff) + first := oldTask.Reminders[0].Reminder + for in, r := range oldTask.Reminders { + diff := r.Reminder.Sub(first) + newTask.Reminders[in].Reminder = now.Add(repeatDuration + diff) } } @@ -1543,9 +1543,10 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder) (err // Loop through all reminders and add them for _, r := range reminderMap { - _, err = s.Insert(&TaskReminder{TaskID: t.ID, Reminder: r.Reminder}) - t.Reminders = append(t.Reminders, &r) - t.ReminderDates = append(t.ReminderDates, r.Reminder) + taskReminder := &TaskReminder{TaskID: t.ID, Reminder: r.Reminder} + _, err = s.Insert(taskReminder) + t.Reminders = append(t.Reminders, taskReminder) + t.ReminderDates = append(t.ReminderDates, taskReminder.Reminder) if err != nil { return err } diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index 835789b14..65c0eb3eb 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -516,9 +516,13 @@ func TestUpdateDone(t *testing.T) { oldTask := &Task{ Done: false, RepeatAfter: 8600, - ReminderDates: []time.Time{ - time.Unix(1550000000, 0), - time.Unix(1555000000, 0), + Reminders: []*TaskReminder{ + { + Reminder: time.Unix(1550000000, 0), + }, + { + Reminder: time.Unix(1555000000, 0), + }, }, } newTask := &Task{ @@ -535,9 +539,9 @@ func TestUpdateDone(t *testing.T) { expected2 = expected2.Add(time.Duration(oldTask.RepeatAfter) * time.Second) } - assert.Len(t, newTask.ReminderDates, 2) - assert.Equal(t, expected1, newTask.ReminderDates[0]) - assert.Equal(t, expected2, newTask.ReminderDates[1]) + assert.Len(t, newTask.Reminders, 2) + assert.Equal(t, expected1, newTask.Reminders[0].Reminder) + assert.Equal(t, expected2, newTask.Reminders[1].Reminder) assert.False(t, newTask.Done) }) t.Run("update start date", func(t *testing.T) { @@ -614,22 +618,25 @@ func TestUpdateDone(t *testing.T) { Done: false, RepeatAfter: 8600, RepeatMode: TaskRepeatModeFromCurrentDate, - ReminderDates: []time.Time{ - time.Unix(1550000000, 0), - time.Unix(1555000000, 0), - }, - } + Reminders: []*TaskReminder{ + { + Reminder: time.Unix(1550000000, 0), + }, + { + Reminder: time.Unix(1555000000, 0), + }, + }} newTask := &Task{ Done: true, } updateDone(oldTask, newTask) - diff := oldTask.ReminderDates[1].Sub(oldTask.ReminderDates[0]) + diff := oldTask.Reminders[1].Reminder.Sub(oldTask.Reminders[0].Reminder) - assert.Len(t, newTask.ReminderDates, 2) + assert.Len(t, newTask.Reminders, 2) // Only comparing unix timestamps because time.Time use nanoseconds which can't ever possibly have the same value - assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.ReminderDates[0].Unix()) - assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.ReminderDates[1].Unix()) + assert.Equal(t, time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[0].Reminder.Unix()) + assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.Reminders[1].Reminder.Unix()) assert.False(t, newTask.Done) }) t.Run("start date", func(t *testing.T) { @@ -707,23 +714,28 @@ func TestUpdateDone(t *testing.T) { oldTask := &Task{ Done: false, RepeatMode: TaskRepeatModeMonth, - ReminderDates: []time.Time{ - time.Unix(1550000000, 0), - time.Unix(1555000000, 0), - }, - } + Reminders: []*TaskReminder{ + { + Reminder: time.Unix(1550000000, 0), + }, + { + Reminder: time.Unix(1555000000, 0), + }, + }} newTask := &Task{ Done: true, } - oldReminders := make([]time.Time, len(oldTask.ReminderDates)) - copy(oldReminders, oldTask.ReminderDates) + oldReminders := make([]time.Time, len(oldTask.Reminders)) + for i, r := range newTask.Reminders { + oldReminders[i] = r.Reminder + } updateDone(oldTask, newTask) - assert.Len(t, newTask.ReminderDates, len(oldReminders)) - for i, r := range newTask.ReminderDates { - assert.True(t, r.After(oldReminders[i])) - assert.NotEqual(t, oldReminders[i].Month(), r.Month()) + assert.Len(t, newTask.Reminders, len(oldReminders)) + for i, r := range newTask.Reminders { + assert.True(t, r.Reminder.After(oldReminders[i])) + assert.NotEqual(t, oldReminders[i].Month(), r.Reminder.Month()) } assert.False(t, newTask.Done) }) diff --git a/pkg/modules/migration/microsoft-todo/microsoft_todo.go b/pkg/modules/migration/microsoft-todo/microsoft_todo.go index e2fd5a1d8..bf3ecd77d 100644 --- a/pkg/modules/migration/microsoft-todo/microsoft_todo.go +++ b/pkg/modules/migration/microsoft-todo/microsoft_todo.go @@ -329,7 +329,9 @@ func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.Name return nil, err } - task.ReminderDates = []time.Time{reminder} + task.Reminders = []*models.TaskReminder{ + {Reminder: reminder}, + } } // Due Date diff --git a/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go b/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go index 7c3a66333..e9f4de0c8 100644 --- a/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go +++ b/pkg/modules/migration/microsoft-todo/microsoft_todo_test.go @@ -141,8 +141,10 @@ func TestConverting(t *testing.T) { { Task: models.Task{ Title: "Task 5", - ReminderDates: []time.Time{ - testtimeTime, + Reminders: []*models.TaskReminder{ + { + Reminder: testtimeTime, + }, }, }, }, diff --git a/pkg/modules/migration/todoist/todoist.go b/pkg/modules/migration/todoist/todoist.go index c0fce8231..3cb5ca14f 100644 --- a/pkg/modules/migration/todoist/todoist.go +++ b/pkg/modules/migration/todoist/todoist.go @@ -471,7 +471,10 @@ func convertTodoistToVikunja(sync *sync, doneItems map[string]*doneItem) (fullVi return nil, err } - tasks[r.ItemID].ReminderDates = append(tasks[r.ItemID].ReminderDates, date.In(config.GetTimeZone())) + tasks[r.ItemID].Reminders = append(tasks[r.ItemID].Reminders, &models.TaskReminder{ + Reminder: date.In(config.GetTimeZone()), + }, + ) } return []*models.NamespaceWithListsAndTasks{ diff --git a/pkg/modules/migration/todoist/todoist_test.go b/pkg/modules/migration/todoist/todoist_test.go index dd697d47f..42d143187 100644 --- a/pkg/modules/migration/todoist/todoist_test.go +++ b/pkg/modules/migration/todoist/todoist_test.go @@ -388,9 +388,9 @@ func TestConvertTodoistToVikunja(t *testing.T) { Description: "Lorem Ipsum dolor sit amet", Done: false, Created: time1, - ReminderDates: []time.Time{ - time.Date(2020, time.June, 15, 23, 59, 0, 0, time.UTC).In(config.GetTimeZone()), - time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), + Reminders: []*models.TaskReminder{ + {Reminder: time.Date(2020, time.June, 15, 23, 59, 0, 0, time.UTC).In(config.GetTimeZone())}, + {Reminder: time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())}, }, }, }, @@ -407,8 +407,8 @@ func TestConvertTodoistToVikunja(t *testing.T) { Title: "Task400000002", Done: false, Created: time1, - ReminderDates: []time.Time{ - time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), + Reminders: []*models.TaskReminder{ + {Reminder: time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())}, }, }, }, @@ -421,8 +421,8 @@ func TestConvertTodoistToVikunja(t *testing.T) { Created: time1, DoneAt: time3, Labels: vikunjaLabels, - ReminderDates: []time.Time{ - time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), + Reminders: []*models.TaskReminder{ + {Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())}, }, }, }, @@ -441,8 +441,8 @@ func TestConvertTodoistToVikunja(t *testing.T) { DueDate: dueTime, Created: time1, DoneAt: time3, - ReminderDates: []time.Time{ - time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), + Reminders: []*models.TaskReminder{ + {Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())}, }, }, }, @@ -531,8 +531,8 @@ func TestConvertTodoistToVikunja(t *testing.T) { Title: "Task400000009", Done: false, Created: time1, - ReminderDates: []time.Time{ - time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()), + Reminders: []*models.TaskReminder{ + {Reminder: time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone())}, }, }, }, -- 2.45.1 From e0487516a330f707cfd6b6a0057dae045f26d341 Mon Sep 17 00:00:00 2001 From: ce72 Date: Tue, 7 Mar 2023 17:54:21 +0100 Subject: [PATCH 04/24] #1416: Step 3a Add new fields to TaskReminder --- pkg/db/fixtures/task_reminders.yml | 2 + pkg/db/fixtures/tasks.yml | 3 +- pkg/integrations/task_collection_test.go | 176 +++++++++--------- pkg/integrations/task_test.go | 10 +- pkg/migration/20230307171848.go | 44 +++++ pkg/models/task_collection_test.go | 15 +- pkg/models/task_reminder.go | 16 +- pkg/models/tasks.go | 41 ++-- pkg/modules/migration/ticktick/ticktick.go | 7 +- .../migration/ticktick/ticktick_test.go | 6 +- pkg/swagger/docs.go | 25 +++ pkg/swagger/swagger.json | 25 +++ pkg/swagger/swagger.yaml | 19 ++ 13 files changed, 265 insertions(+), 124 deletions(-) create mode 100644 pkg/migration/20230307171848.go diff --git a/pkg/db/fixtures/task_reminders.yml b/pkg/db/fixtures/task_reminders.yml index b3c158fbc..d3d023e9b 100644 --- a/pkg/db/fixtures/task_reminders.yml +++ b/pkg/db/fixtures/task_reminders.yml @@ -6,6 +6,8 @@ task_id: 27 reminder: 2018-12-01 01:13:44 created: 2018-12-01 01:12:04 + relative_to: 'start_date' + relative_period: -3600 - id: 3 task_id: 2 reminder: 2018-12-01 01:13:44 diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml index 10e81b2f4..d3d4f5ad2 100644 --- a/pkg/db/fixtures/tasks.yml +++ b/pkg/db/fixtures/tasks.yml @@ -244,7 +244,7 @@ created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 - id: 27 - title: 'task #27 with reminders' + title: 'task #27 with reminders and start_date' done: false created_by_id: 1 list_id: 1 @@ -252,6 +252,7 @@ bucket_id: 1 created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 + start_date: 2018-11-30 22:25:24 - id: 28 title: 'task #28 with repeat after' done: false diff --git a/pkg/integrations/task_collection_test.go b/pkg/integrations/task_collection_test.go index 5dbb36991..d14d7d4ef 100644 --- a/pkg/integrations/task_collection_test.go +++ b/pkg/integrations/task_collection_test.go @@ -42,14 +42,14 @@ func TestTaskCollection(t *testing.T) { assert.NoError(t, err) // Not using assert.Equal to avoid having the tests break every time we add new fixtures assert.Contains(t, rec.Body.String(), `task #1`) - assert.Contains(t, rec.Body.String(), `task #2`) - assert.Contains(t, rec.Body.String(), `task #3`) - assert.Contains(t, rec.Body.String(), `task #4`) - assert.Contains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.Contains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.Contains(t, rec.Body.String(), `task #2 `) + assert.Contains(t, rec.Body.String(), `task #3 `) + assert.Contains(t, rec.Body.String(), `task #4 `) + assert.Contains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.Contains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.Contains(t, rec.Body.String(), `task #10`) assert.Contains(t, rec.Body.String(), `task #11`) assert.Contains(t, rec.Body.String(), `task #12`) @@ -75,14 +75,14 @@ func TestTaskCollection(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, urlParams) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.NotContains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.NotContains(t, rec.Body.String(), `task #7`) - assert.NotContains(t, rec.Body.String(), `task #8`) - assert.NotContains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.NotContains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.NotContains(t, rec.Body.String(), `task #7 `) + assert.NotContains(t, rec.Body.String(), `task #8 `) + assert.NotContains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -93,14 +93,14 @@ func TestTaskCollection(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"tASk #6"}}, urlParams) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.NotContains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.NotContains(t, rec.Body.String(), `task #7`) - assert.NotContains(t, rec.Body.String(), `task #8`) - assert.NotContains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.NotContains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.NotContains(t, rec.Body.String(), `task #7 `) + assert.NotContains(t, rec.Body.String(), `task #8 `) + assert.NotContains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -190,14 +190,14 @@ func TestTaskCollection(t *testing.T) { ) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.Contains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.Contains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.Contains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.Contains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -215,14 +215,14 @@ func TestTaskCollection(t *testing.T) { ) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.NotContains(t, rec.Body.String(), `task #5`) - assert.NotContains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.NotContains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.NotContains(t, rec.Body.String(), `task #5 `) + assert.NotContains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.NotContains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -255,14 +255,14 @@ func TestTaskCollection(t *testing.T) { ) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.Contains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.NotContains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.Contains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.NotContains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -291,14 +291,14 @@ func TestTaskCollection(t *testing.T) { ) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.Contains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.Contains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.Contains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.Contains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -314,14 +314,14 @@ func TestTaskCollection(t *testing.T) { assert.NoError(t, err) // Not using assert.Equal to avoid having the tests break every time we add new fixtures assert.Contains(t, rec.Body.String(), `task #1`) - assert.Contains(t, rec.Body.String(), `task #2`) - assert.Contains(t, rec.Body.String(), `task #3`) - assert.Contains(t, rec.Body.String(), `task #4`) - assert.Contains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.Contains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.Contains(t, rec.Body.String(), `task #2 `) + assert.Contains(t, rec.Body.String(), `task #3 `) + assert.Contains(t, rec.Body.String(), `task #4 `) + assert.Contains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.Contains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.Contains(t, rec.Body.String(), `task #10`) assert.Contains(t, rec.Body.String(), `task #11`) assert.Contains(t, rec.Body.String(), `task #12`) @@ -347,14 +347,14 @@ func TestTaskCollection(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, nil) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.NotContains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.NotContains(t, rec.Body.String(), `task #7`) - assert.NotContains(t, rec.Body.String(), `task #8`) - assert.NotContains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.NotContains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.NotContains(t, rec.Body.String(), `task #7 `) + assert.NotContains(t, rec.Body.String(), `task #8 `) + assert.NotContains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -417,14 +417,14 @@ func TestTaskCollection(t *testing.T) { ) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.Contains(t, rec.Body.String(), `task #5`) - assert.Contains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.Contains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.Contains(t, rec.Body.String(), `task #5 `) + assert.Contains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.Contains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) @@ -442,14 +442,14 @@ func TestTaskCollection(t *testing.T) { ) assert.NoError(t, err) assert.NotContains(t, rec.Body.String(), `task #1`) - assert.NotContains(t, rec.Body.String(), `task #2`) - assert.NotContains(t, rec.Body.String(), `task #3`) - assert.NotContains(t, rec.Body.String(), `task #4`) - assert.NotContains(t, rec.Body.String(), `task #5`) - assert.NotContains(t, rec.Body.String(), `task #6`) - assert.Contains(t, rec.Body.String(), `task #7`) - assert.NotContains(t, rec.Body.String(), `task #8`) - assert.Contains(t, rec.Body.String(), `task #9`) + assert.NotContains(t, rec.Body.String(), `task #2 `) + assert.NotContains(t, rec.Body.String(), `task #3 `) + assert.NotContains(t, rec.Body.String(), `task #4 `) + assert.NotContains(t, rec.Body.String(), `task #5 `) + assert.NotContains(t, rec.Body.String(), `task #6 `) + assert.Contains(t, rec.Body.String(), `task #7 `) + assert.NotContains(t, rec.Body.String(), `task #8 `) + assert.Contains(t, rec.Body.String(), `task #9 `) assert.NotContains(t, rec.Body.String(), `task #10`) assert.NotContains(t, rec.Body.String(), `task #11`) assert.NotContains(t, rec.Body.String(), `task #12`) diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go index f2cc3eb97..c575dfb6e 100644 --- a/pkg/integrations/task_test.go +++ b/pkg/integrations/task_test.go @@ -117,24 +117,24 @@ func TestTask(t *testing.T) { assert.NotContains(t, rec.Body.String(), `"reminder_dates":[1543626724,1543626824]`) }) t.Run("Reminders", func(t *testing.T) { - rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminders": [{"Reminder": "2020-02-10T10:00:00Z"},{"Reminder": "2020-02-11T10:00:00Z"}]}`) + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminders": [{"reminder": "2020-02-10T10:00:00Z"},{"reminder": "2020-02-11T10:00:00Z"}]}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminders":[`) - assert.Contains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"}`) - assert.Contains(t, rec.Body.String(), `{"Reminder":"2020-02-11T10:00:00Z"}`) + assert.Contains(t, rec.Body.String(), `{"reminder":"2020-02-10T10:00:00Z"`) + assert.Contains(t, rec.Body.String(), `{"reminder":"2020-02-11T10:00:00Z"`) assert.NotContains(t, rec.Body.String(), `"reminders":null`) }) t.Run("Reminders unset to empty array", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminders": []}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminders":null`) - assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"}`) + assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"`) }) t.Run("Reminders unset to null", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminders": null}`) assert.NoError(t, err) assert.Contains(t, rec.Body.String(), `"reminder_dates":null`) - assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"}`) + assert.NotContains(t, rec.Body.String(), `{"Reminder":"2020-02-10T10:00:00Z"`) }) t.Run("Repeat after", func(t *testing.T) { rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"repeat_after":3600}`) diff --git a/pkg/migration/20230307171848.go b/pkg/migration/20230307171848.go new file mode 100644 index 000000000..2a55751ec --- /dev/null +++ b/pkg/migration/20230307171848.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 migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type taskReminders20230307171848 struct { + RelativePeriod int64 `xorm:"bigint null" json:"relative_period"` + RelativeTo string `xorm:"varchar(50) null" json:"relative_to,omitempty"` +} + +func (taskReminders20230307171848) TableName() string { + return "task_reminders" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20230307171848", + Description: "Add relative period to task reminders", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(taskReminders20230307171848{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index e8a65e27f..5091dfbb7 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -480,7 +480,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { } task27 := &Task{ ID: 27, - Title: "task #27 with reminders", + Title: "task #27 with reminders and start_date", Identifier: "test1-12", Index: 12, CreatedByID: 1, @@ -497,12 +497,15 @@ func TestTaskCollection_ReadAll(t *testing.T) { Created: time.Unix(1543626724, 0).In(loc), }, { - ID: 2, - TaskID: 27, - Reminder: time.Unix(1543626824, 0).In(loc), - Created: time.Unix(1543626724, 0).In(loc), + ID: 2, + TaskID: 27, + Reminder: time.Unix(1543626824, 0).In(loc), + Created: time.Unix(1543626724, 0).In(loc), + RelativePeriod: -3600, + RelativeTo: "start_date", }, }, + StartDate: time.Unix(1543616724, 0).In(loc), ListID: 1, BucketID: 1, RelatedTasks: map[RelationKind][]*Task{}, @@ -1268,7 +1271,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { return } - t.Errorf("Test %s, Task.ReadAll() = %v, want %v, \ndiff: %v", tt.name, got, tt.want, diff) + t.Errorf("Test %s, Task.ReadAll() = %v, \nwant %v, \ndiff: %v", tt.name, got, tt.want, diff) } }) } diff --git a/pkg/models/task_reminder.go b/pkg/models/task_reminder.go index 56d7180bb..dba9c3cb4 100644 --- a/pkg/models/task_reminder.go +++ b/pkg/models/task_reminder.go @@ -33,13 +33,27 @@ import ( "code.vikunja.io/api/pkg/user" ) +// ReminderRelation represents the task attribute which the period based reminder relates to +type ReminderRelation string + +// All valid ReminderRelations +const ( + DueDate ReminderRelation = `due_date` + StartDate ReminderRelation = `start_date` + EndDate ReminderRelation = `end_date` +) + // TaskReminder holds a reminder on a task type TaskReminder struct { ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"` TaskID int64 `xorm:"bigint not null INDEX" json:"-"` // The absolute time when the user wants to be reminded of the task. - Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'"` + Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'" json:"reminder"` Created time.Time `xorm:"created not null" json:"-"` + // A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date, positive after. + RelativePeriod int64 `xorm:"bigint null" json:"relative_period"` + // The name of the date field to which the relative period refers to. + RelativeTo ReminderRelation `xorm:"varchar(50) null" json:"relative_to"` } // TableName returns a pretty table name diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index fdb6f8492..5a8a19b93 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -973,14 +973,12 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err } } - // Update the reminders - // - // Deprecated: the if clause can be removed if ReminderDates will be removed + // Deprecated: the if clause can be removed when ReminderDates will be removed if t.ReminderDates != nil { - if err := t.updateRemindersFromReminderDates(t.ReminderDates); err != nil { - return err - } + t.updateRemindersFromReminderDates(t.ReminderDates) } + + // Update the reminders if err := t.updateReminders(s, t.Reminders); err != nil { return err } @@ -1068,12 +1066,11 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { return err } - // Deprecated: This statement can be removed if ReminderDates will be removed + // Deprecated: This statement can be removed when ReminderDates will be removed if t.ReminderDates != nil { - if err := t.updateRemindersFromReminderDates(t.ReminderDates); err != nil { - return err - } + t.updateRemindersFromReminderDates(t.ReminderDates) } + // Update the reminders if err := ot.updateReminders(s, t.Reminders); err != nil { return err @@ -1505,18 +1502,24 @@ func updateDone(oldTask *Task, newTask *Task) { } // Deprecated: will be removed when ReminderDates are removed from Task. -// For now the method just creates Taskeminder Objects from the ReminderDates -func (t *Task) updateRemindersFromReminderDates(reminders []time.Time) (err error) { +// For now the method just creates Taskeminder objects from the ReminderDates +func (t *Task) updateRemindersFromReminderDates(reminderDates []time.Time) { + // if the client still sends old reminder_dates, then these will overwrite the absolute triggers + // of the Reminders, sent by the client. We assume that clients still sending old reminder_dates do not + // understand the new reminders. - for _, reminder := range reminders { - t.Reminders = append(t.Reminders, &TaskReminder{TaskID: t.ID, Reminder: reminder}) + // remove absolute triggers in Reminders + updatedReminders := make([]*TaskReminder, 0) + for _, reminder := range t.Reminders { + if reminder.RelativeTo != "" { + updatedReminders = append(updatedReminders, reminder) + } } - - t.ReminderDates = reminders - if len(reminders) == 0 { - t.ReminderDates = nil + // append absolute triggers from ReminderDates + for _, reminderDate := range reminderDates { + updatedReminders = append(updatedReminders, &TaskReminder{TaskID: t.ID, Reminder: reminderDate}) } - return + t.Reminders = updatedReminders } // Removes all old reminders and adds the new ones. This is a lot easier and less buggy than diff --git a/pkg/modules/migration/ticktick/ticktick.go b/pkg/modules/migration/ticktick/ticktick.go index 0870ae87d..91bbf9972 100644 --- a/pkg/modules/migration/ticktick/ticktick.go +++ b/pkg/modules/migration/ticktick/ticktick.go @@ -147,8 +147,11 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.Namespace } if !t.DueDate.IsZero() && t.Reminder > 0 { - task.Task.ReminderDates = []time.Time{ - t.DueDate.Add(t.Reminder * -1), + task.Task.Reminders = []*models.TaskReminder{ + { + RelativeTo: models.DueDate, + RelativePeriod: int64((t.Reminder * -1).Seconds()), + }, } } diff --git a/pkg/modules/migration/ticktick/ticktick_test.go b/pkg/modules/migration/ticktick/ticktick_test.go index 1638b3769..1ee2341ac 100644 --- a/pkg/modules/migration/ticktick/ticktick_test.go +++ b/pkg/modules/migration/ticktick/ticktick_test.go @@ -101,7 +101,8 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) { {Title: "label1"}, {Title: "label2"}, }) - //assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].ReminderDates, tickTickTasks[0].) // TODO + assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Reminders[0].RelativeTo, models.ReminderRelation("due_date")) + assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Reminders[0].RelativePeriod, int64(-24*3600)) assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Position, tickTickTasks[0].Order) assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].Done, false) @@ -127,7 +128,8 @@ func TestConvertTicktickTasksToVikunja(t *testing.T) { {Title: "label2"}, {Title: "other label"}, }) - //assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[0].ReminderDates, tickTickTasks[0].) // TODO + assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Reminders[0].RelativeTo, models.ReminderRelation("due_date")) + assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Reminders[0].RelativePeriod, int64(-24*3600)) assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Position, tickTickTasks[2].Order) assert.Equal(t, vikunjaTasks[0].Lists[0].Tasks[2].Done, false) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index dd934ae38..7d70fee0c 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -8309,6 +8309,19 @@ const docTemplate = `{ "RelationKindCopiedTo" ] }, + "models.ReminderRelation": { + "type": "string", + "enum": [ + "due_date", + "start_date", + "end_date" + ], + "x-enum-varnames": [ + "DueDate", + "StartDate", + "EndDate" + ] + }, "models.Right": { "type": "integer", "enum": [ @@ -8707,6 +8720,18 @@ const docTemplate = `{ "models.TaskReminder": { "type": "object", "properties": { + "relative_period": { + "description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date, positive after.", + "type": "integer" + }, + "relative_to": { + "description": "The name of the date field to which the relative period refers to.", + "allOf": [ + { + "$ref": "#/definitions/models.ReminderRelation" + } + ] + }, "reminder": { "description": "The absolute time when the user wants to be reminded of the task.", "type": "string" diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 00ac515b7..0e09adecc 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -8301,6 +8301,19 @@ "RelationKindCopiedTo" ] }, + "models.ReminderRelation": { + "type": "string", + "enum": [ + "due_date", + "start_date", + "end_date" + ], + "x-enum-varnames": [ + "DueDate", + "StartDate", + "EndDate" + ] + }, "models.Right": { "type": "integer", "enum": [ @@ -8699,6 +8712,18 @@ "models.TaskReminder": { "type": "object", "properties": { + "relative_period": { + "description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date, positive after.", + "type": "integer" + }, + "relative_to": { + "description": "The name of the date field to which the relative period refers to.", + "allOf": [ + { + "$ref": "#/definitions/models.ReminderRelation" + } + ] + }, "reminder": { "description": "The absolute time when the user wants to be reminded of the task.", "type": "string" diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 9811d4ca2..8dbd01881 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -602,6 +602,16 @@ definitions: - RelationKindFollows - RelationKindCopiedFrom - RelationKindCopiedTo + models.ReminderRelation: + enum: + - due_date + - start_date + - end_date + type: string + x-enum-varnames: + - DueDate + - StartDate + - EndDate models.Right: enum: - 0 @@ -904,6 +914,15 @@ definitions: type: object models.TaskReminder: properties: + relative_period: + description: A period in seconds relative to another date argument. Negative + values mean the reminder triggers before the date, positive after. + type: integer + relative_to: + allOf: + - $ref: '#/definitions/models.ReminderRelation' + description: The name of the date field to which the relative period refers + to. reminder: description: The absolute time when the user wants to be reminded of the task. type: string -- 2.45.1 From d8630e96804b4f1082e3525a50850f975bb180d4 Mon Sep 17 00:00:00 2001 From: ce72 Date: Tue, 7 Mar 2023 19:50:49 +0100 Subject: [PATCH 05/24] fix: task filter "reminders" uses new Reminder object --- pkg/models/task_collection_filter.go | 7 +++++++ pkg/models/task_collection_test.go | 2 +- pkg/models/tasks.go | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go index 4a4dd1ccc..3e5331eb9 100644 --- a/pkg/models/task_collection_filter.go +++ b/pkg/models/task_collection_filter.go @@ -266,6 +266,13 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato return nil, nil, ErrInvalidTaskField{TaskField: fieldName} } + if realFieldName == "Reminders" { + field, ok = reflect.TypeOf(&TaskReminder{}).Elem().FieldByName("Reminder") + if !ok { + return nil, nil, ErrInvalidTaskField{TaskField: fieldName} + } + } + if comparator == taskFilterComparatorIn { vals := strings.Split(value, ",") valueSlice := []interface{}{} diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index 5091dfbb7..5a1450762 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -933,7 +933,7 @@ func TestTaskCollection_ReadAll(t *testing.T) { { name: "filtered reminder dates", fields: fields{ - FilterBy: []string{"ReminderDates", "ReminderDates"}, + FilterBy: []string{"reminders", "reminders"}, FilterValue: []string{"2018-10-01T00:00:00+00:00", "2018-12-10T00:00:00+00:00"}, FilterComparator: []string{"greater", "less"}, }, diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 5a8a19b93..83a413037 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -338,7 +338,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO var filters = make([]builder.Cond, 0, len(opts.filters)) // To still find tasks with nil values, we exclude 0s when comparing with >/< values. for _, f := range opts.filters { - if f.field == "ReminderDates" { + if f.field == "reminders" { f.field = "reminder" // This is the name in the db filter, err := getFilterCond(f, opts.filterIncludeNulls) if err != nil { -- 2.45.1 From e5cec4bd163548b0596311c2a6e710a3667c4d14 Mon Sep 17 00:00:00 2001 From: ce72 Date: Tue, 7 Mar 2023 21:24:47 +0100 Subject: [PATCH 06/24] #1416: Step 4 Update reminder time in relative reminders --- pkg/models/task_reminder.go | 6 +- pkg/models/tasks.go | 30 +++++- pkg/models/tasks_test.go | 119 +++++++++++++++------ pkg/modules/migration/ticktick/ticktick.go | 2 +- pkg/swagger/docs.go | 6 +- pkg/swagger/swagger.json | 6 +- pkg/swagger/swagger.yaml | 6 +- 7 files changed, 125 insertions(+), 50 deletions(-) diff --git a/pkg/models/task_reminder.go b/pkg/models/task_reminder.go index dba9c3cb4..0f0e70c28 100644 --- a/pkg/models/task_reminder.go +++ b/pkg/models/task_reminder.go @@ -38,9 +38,9 @@ type ReminderRelation string // All valid ReminderRelations const ( - DueDate ReminderRelation = `due_date` - StartDate ReminderRelation = `start_date` - EndDate ReminderRelation = `end_date` + ReminderDueDate ReminderRelation = `due_date` + ReminderStartDate ReminderRelation = `start_date` + ReminderEndDate ReminderRelation = `end_date` ) // TaskReminder holds a reminder on a task diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 83a413037..6f43d5e2c 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -979,7 +979,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err } // Update the reminders - if err := t.updateReminders(s, t.Reminders); err != nil { + if err := t.updateReminders(s, t.Reminders, t.DueDate, t.StartDate, t.EndDate); err != nil { return err } @@ -1072,7 +1072,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { } // Update the reminders - if err := ot.updateReminders(s, t.Reminders); err != nil { + if err := ot.updateReminders(s, t.Reminders, t.DueDate, t.StartDate, t.EndDate); err != nil { return err } @@ -1525,8 +1525,8 @@ func (t *Task) updateRemindersFromReminderDates(reminderDates []time.Time) { // Removes all old reminders and adds the new ones. This is a lot easier and less buggy than // trying to figure out which reminders changed and then only re-add those needed. And since it does // not make a performance difference we'll just do that. -// The parameter is a slice with unix dates which holds the new reminders. -func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder) (err error) { +// The parameter is a slice which holds the new reminders. +func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDate time.Time, startDate time.Time, endDate time.Time) (err error) { _, err = s. Where("task_id = ?", t.ID). @@ -1535,6 +1535,8 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder) (err return } + setRelativeReminderDates(reminders, dueDate, startDate, endDate) + // Resolve duplicates and sort them reminderMap := make(map[int64]TaskReminder, len(reminders)) for _, reminder := range reminders { @@ -1564,6 +1566,26 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder) (err return } +func setRelativeReminderDates(reminders []*TaskReminder, dueDate time.Time, startDate time.Time, endDate time.Time) { + for _, reminder := range reminders { + relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second + switch reminder.RelativeTo { + case ReminderDueDate: + if !dueDate.IsZero() { + reminder.Reminder = dueDate.Add(relativeDuration) + } + case ReminderStartDate: + if !startDate.IsZero() { + reminder.Reminder = startDate.Add(relativeDuration) + } + case ReminderEndDate: + if !endDate.IsZero() { + reminder.Reminder = endDate.Add(relativeDuration) + } + } + } +} + func updateTaskLastUpdated(s *xorm.Session, task *Task) error { _, err := s.ID(task.ID).Cols("updated").Update(task) return err diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index 65c0eb3eb..06318197f 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -70,6 +70,44 @@ func TestTask_Create(t *testing.T) { events.AssertDispatched(t, &TaskCreatedEvent{}) }) + t.Run("with reminders", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + task := &Task{ + Title: "Lorem", + Description: "Lorem Ipsum Dolor", + ListID: 1, + DueDate: time.Unix(1550000000, 0), + StartDate: time.Unix(1550000010, 0), + EndDate: time.Unix(1550000020, 0), + Reminders: []*TaskReminder{ + { + RelativeTo: "due_date", + RelativePeriod: 1, + }, + { + RelativeTo: "start_date", + RelativePeriod: -2, + }, + { + RelativeTo: "end_date", + RelativePeriod: -1, + }, + { + Reminder: time.Unix(1550000020, 0), + }, + }} + err := task.Create(s, usr) + assert.NoError(t, err) + assert.Equal(t, task.Reminders[0].Reminder, time.Unix(1550000001, 0)) + assert.Equal(t, task.Reminders[1].Reminder, time.Unix(1550000008, 0)) + assert.Equal(t, task.Reminders[2].Reminder, time.Unix(1550000019, 0)) + assert.Equal(t, task.Reminders[3].Reminder, time.Unix(1550000020, 0)) + err = s.Commit() + assert.NoError(t, err) + }) t.Run("empty title", func(t *testing.T) { db.LoadAndAssertFixtures(t) s := db.NewSession() @@ -369,8 +407,47 @@ func TestTask_Update(t *testing.T) { assert.Equal(t, int64(3), task.Index) }) - // Deprecated: see next test - t.Run("the same reminder date multiple times should be saved once", func(t *testing.T) { + t.Run("reminders will be updated", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + task := &Task{ + ID: 1, + ListID: 1, + Title: "test", + DueDate: time.Unix(1550000000, 0), + StartDate: time.Unix(1550000010, 0), + EndDate: time.Unix(1550000020, 0), + Reminders: []*TaskReminder{ + { + RelativeTo: "due_date", + RelativePeriod: 1, + }, + { + RelativeTo: "start_date", + RelativePeriod: -2, + }, + { + RelativeTo: "end_date", + RelativePeriod: -1, + }, + { + Reminder: time.Unix(1550000020, 0), + }, + }, + } + err := task.Update(s, u) + assert.NoError(t, err) + assert.Equal(t, task.Reminders[0].Reminder, time.Unix(1550000001, 0)) + assert.Equal(t, task.Reminders[1].Reminder, time.Unix(1550000008, 0)) + assert.Equal(t, task.Reminders[2].Reminder, time.Unix(1550000019, 0)) + assert.Equal(t, task.Reminders[3].Reminder, time.Unix(1550000020, 0)) + err = s.Commit() + assert.NoError(t, err) + db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 4) + }) + t.Run("the same reminder multiple times should be saved once", func(t *testing.T) { db.LoadAndAssertFixtures(t) s := db.NewSession() defer s.Close() @@ -378,9 +455,13 @@ func TestTask_Update(t *testing.T) { task := &Task{ ID: 1, Title: "test", - ReminderDates: []time.Time{ - time.Unix(1674745156, 0), - time.Unix(1674745156, 223), + Reminders: []*TaskReminder{ + { + Reminder: time.Unix(1674745156, 0), + }, + { + Reminder: time.Unix(1674745156, 223), + }, }, ListID: 1, } @@ -388,34 +469,6 @@ func TestTask_Update(t *testing.T) { assert.NoError(t, err) err = s.Commit() assert.NoError(t, err) - - db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 1) - }) - - t.Run("the same reminder multiple times should be saved once", func(t *testing.T) { - db.LoadAndAssertFixtures(t) - s := db.NewSession() - defer s.Close() - - taskReminders := []*TaskReminder{ - { - Reminder: time.Unix(1674745156, 0), - }, - { - Reminder: time.Unix(1674745156, 223), - }, - } - - task := &Task{ - ID: 1, - Title: "test", - Reminders: taskReminders, - ListID: 1, - } - err := task.Update(s, u) - assert.NoError(t, err) - err = s.Commit() - assert.NoError(t, err) db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 1) }) } diff --git a/pkg/modules/migration/ticktick/ticktick.go b/pkg/modules/migration/ticktick/ticktick.go index 91bbf9972..8036ffa88 100644 --- a/pkg/modules/migration/ticktick/ticktick.go +++ b/pkg/modules/migration/ticktick/ticktick.go @@ -149,7 +149,7 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.Namespace if !t.DueDate.IsZero() && t.Reminder > 0 { task.Task.Reminders = []*models.TaskReminder{ { - RelativeTo: models.DueDate, + RelativeTo: models.ReminderDueDate, RelativePeriod: int64((t.Reminder * -1).Seconds()), }, } diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 7d70fee0c..728221ea7 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -8317,9 +8317,9 @@ const docTemplate = `{ "end_date" ], "x-enum-varnames": [ - "DueDate", - "StartDate", - "EndDate" + "ReminderDueDate", + "ReminderStartDate", + "ReminderEndDate" ] }, "models.Right": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 0e09adecc..773189696 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -8309,9 +8309,9 @@ "end_date" ], "x-enum-varnames": [ - "DueDate", - "StartDate", - "EndDate" + "ReminderDueDate", + "ReminderStartDate", + "ReminderEndDate" ] }, "models.Right": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 8dbd01881..a7b1a80e5 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -609,9 +609,9 @@ definitions: - end_date type: string x-enum-varnames: - - DueDate - - StartDate - - EndDate + - ReminderDueDate + - ReminderStartDate + - ReminderEndDate models.Right: enum: - 0 -- 2.45.1 From 3c4fb76554a70de89142f2131e1a9d9a2e720c3f Mon Sep 17 00:00:00 2001 From: ce72 Date: Tue, 7 Mar 2023 22:27:19 +0100 Subject: [PATCH 07/24] fix: sort reminders --- pkg/models/tasks.go | 4 ++++ pkg/models/tasks_test.go | 35 +++++++++++++++++------------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 6f43d5e2c..0713a5cf8 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1556,6 +1556,10 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDa return err } } + // sort reminders + sort.Slice(t.Reminders, func(i, j int) bool { + return t.Reminders[i].Reminder.Before(t.Reminders[j].Reminder) + }) if len(reminders) == 0 { t.Reminders = nil diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index 06318197f..84f9df6a5 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -79,9 +79,9 @@ func TestTask_Create(t *testing.T) { Title: "Lorem", Description: "Lorem Ipsum Dolor", ListID: 1, - DueDate: time.Unix(1550000000, 0), - StartDate: time.Unix(1550000010, 0), - EndDate: time.Unix(1550000020, 0), + DueDate: time.Date(2023, time.March, 7, 22, 5, 0, 0, time.Local), + StartDate: time.Date(2023, time.March, 7, 22, 5, 10, 0, time.Local), + EndDate: time.Date(2023, time.March, 7, 22, 5, 20, 0, time.Local), Reminders: []*TaskReminder{ { RelativeTo: "due_date", @@ -96,15 +96,15 @@ func TestTask_Create(t *testing.T) { RelativePeriod: -1, }, { - Reminder: time.Unix(1550000020, 0), + Reminder: time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), }, }} err := task.Create(s, usr) assert.NoError(t, err) - assert.Equal(t, task.Reminders[0].Reminder, time.Unix(1550000001, 0)) - assert.Equal(t, task.Reminders[1].Reminder, time.Unix(1550000008, 0)) - assert.Equal(t, task.Reminders[2].Reminder, time.Unix(1550000019, 0)) - assert.Equal(t, task.Reminders[3].Reminder, time.Unix(1550000020, 0)) + assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder) + assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder) + assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder) + assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder) err = s.Commit() assert.NoError(t, err) }) @@ -416,9 +416,9 @@ func TestTask_Update(t *testing.T) { ID: 1, ListID: 1, Title: "test", - DueDate: time.Unix(1550000000, 0), - StartDate: time.Unix(1550000010, 0), - EndDate: time.Unix(1550000020, 0), + DueDate: time.Date(2023, time.March, 7, 22, 5, 0, 0, time.Local), + StartDate: time.Date(2023, time.March, 7, 22, 5, 10, 0, time.Local), + EndDate: time.Date(2023, time.March, 7, 22, 5, 20, 0, time.Local), Reminders: []*TaskReminder{ { RelativeTo: "due_date", @@ -433,16 +433,15 @@ func TestTask_Update(t *testing.T) { RelativePeriod: -1, }, { - Reminder: time.Unix(1550000020, 0), + Reminder: time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), }, - }, - } + }} err := task.Update(s, u) assert.NoError(t, err) - assert.Equal(t, task.Reminders[0].Reminder, time.Unix(1550000001, 0)) - assert.Equal(t, task.Reminders[1].Reminder, time.Unix(1550000008, 0)) - assert.Equal(t, task.Reminders[2].Reminder, time.Unix(1550000019, 0)) - assert.Equal(t, task.Reminders[3].Reminder, time.Unix(1550000020, 0)) + assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder) + assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder) + assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder) + assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder) err = s.Commit() assert.NoError(t, err) db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 4) -- 2.45.1 From 8674b6437e9475483bf5e2452c6172bec42d0ed1 Mon Sep 17 00:00:00 2001 From: ce72 Date: Tue, 7 Mar 2023 23:45:09 +0100 Subject: [PATCH 08/24] fix: store reminder_period and reminder_to --- pkg/models/tasks.go | 6 +++++- pkg/models/tasks_test.go | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 0713a5cf8..8e429c50c 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1548,7 +1548,11 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDa // Loop through all reminders and add them for _, r := range reminderMap { - taskReminder := &TaskReminder{TaskID: t.ID, Reminder: r.Reminder} + taskReminder := &TaskReminder{ + TaskID: t.ID, + Reminder: r.Reminder, + RelativePeriod: r.RelativePeriod, + RelativeTo: r.RelativeTo} _, err = s.Insert(taskReminder) t.Reminders = append(t.Reminders, taskReminder) t.ReminderDates = append(t.ReminderDates, taskReminder.Reminder) diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index 84f9df6a5..84b7a79cb 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -102,6 +102,8 @@ func TestTask_Create(t *testing.T) { err := task.Create(s, usr) assert.NoError(t, err) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder) + assert.Equal(t, int64(1), task.Reminders[0].RelativePeriod) + assert.Equal(t, ReminderDueDate, task.Reminders[0].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder) assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder) @@ -439,6 +441,8 @@ func TestTask_Update(t *testing.T) { err := task.Update(s, u) assert.NoError(t, err) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder) + assert.Equal(t, int64(1), task.Reminders[0].RelativePeriod) + assert.Equal(t, ReminderDueDate, task.Reminders[0].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder) assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder) -- 2.45.1 From 54ed002d1bfb1215596c5ba485833efe76407fcb Mon Sep 17 00:00:00 2001 From: ce72 Date: Wed, 8 Mar 2023 12:44:05 +0100 Subject: [PATCH 09/24] fix: add tests and cleanup --- pkg/models/task_reminder.go | 14 ++-- pkg/models/tasks.go | 81 +++++++++++----------- pkg/models/tasks_test.go | 42 ++++++++++- pkg/modules/migration/ticktick/ticktick.go | 2 +- pkg/swagger/docs.go | 12 ++-- pkg/swagger/swagger.json | 12 ++-- pkg/swagger/swagger.yaml | 15 ++-- 7 files changed, 109 insertions(+), 69 deletions(-) diff --git a/pkg/models/task_reminder.go b/pkg/models/task_reminder.go index 0f0e70c28..917e37c6e 100644 --- a/pkg/models/task_reminder.go +++ b/pkg/models/task_reminder.go @@ -33,24 +33,26 @@ import ( "code.vikunja.io/api/pkg/user" ) -// ReminderRelation represents the task attribute which the period based reminder relates to +// ReminderRelation represents the date attribute of the task which a period based reminder relates to type ReminderRelation string // All valid ReminderRelations const ( - ReminderDueDate ReminderRelation = `due_date` - ReminderStartDate ReminderRelation = `start_date` - ReminderEndDate ReminderRelation = `end_date` + ReminderRelationDueDate ReminderRelation = `due_date` + ReminderRelationStartDate ReminderRelation = `start_date` + ReminderRelationEndDate ReminderRelation = `end_date` ) -// TaskReminder holds a reminder on a task +// TaskReminder holds a reminder on a task. +// If RelativeTo and the assciated date field are defined, then the attribute Reminder will be computed. +// If RelativeTo is missing, than Reminder must be given. type TaskReminder struct { ID int64 `xorm:"bigint autoincr not null unique pk" json:"-"` TaskID int64 `xorm:"bigint not null INDEX" json:"-"` // The absolute time when the user wants to be reminded of the task. Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'" json:"reminder"` Created time.Time `xorm:"created not null" json:"-"` - // A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date, positive after. + // A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date. Default: 0, tiggers when RelativeTo is due. RelativePeriod int64 `xorm:"bigint null" json:"relative_period"` // The name of the date field to which the relative period refers to. RelativeTo ReminderRelation `xorm:"varchar(50) null" json:"relative_to"` diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 8e429c50c..9762fb2a8 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -63,7 +63,7 @@ type Task struct { DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"` // An array of datetimes when the user wants to be reminded of the task. // - // Deprecated: Use Reminders[] + // Deprecated: Use Reminders ReminderDates []time.Time `xorm:"-" json:"reminder_dates"` // An array of reminders that are associated with this task. Reminders []*TaskReminder `xorm:"-" json:"reminders"` @@ -975,7 +975,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err // Deprecated: the if clause can be removed when ReminderDates will be removed if t.ReminderDates != nil { - t.updateRemindersFromReminderDates(t.ReminderDates) + t.overwriteRemindersWithReminderDates(t.ReminderDates) } // Update the reminders @@ -1068,7 +1068,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { // Deprecated: This statement can be removed when ReminderDates will be removed if t.ReminderDates != nil { - t.updateRemindersFromReminderDates(t.ReminderDates) + t.overwriteRemindersWithReminderDates(t.ReminderDates) } // Update the reminders @@ -1502,24 +1502,42 @@ func updateDone(oldTask *Task, newTask *Task) { } // Deprecated: will be removed when ReminderDates are removed from Task. -// For now the method just creates Taskeminder objects from the ReminderDates -func (t *Task) updateRemindersFromReminderDates(reminderDates []time.Time) { - // if the client still sends old reminder_dates, then these will overwrite the absolute triggers - // of the Reminders, sent by the client. We assume that clients still sending old reminder_dates do not - // understand the new reminders. +// For now the method just creates TaskReminder objects from the ReminderDates and overwrites Reminder. +func (t *Task) overwriteRemindersWithReminderDates(reminderDates []time.Time) { + // If the client still sends old reminder_dates, then these will overwrite + // the Reminders, if the were sent by the client, too. + // We assume that clients still using the old API with reminder_dates do not understand the new reminders. + // Clients who want to use the new Reminder structure must explicitey unset reminder_dates. + + // start with empty Reminders + reminders := make([]*TaskReminder, 0) - // remove absolute triggers in Reminders - updatedReminders := make([]*TaskReminder, 0) - for _, reminder := range t.Reminders { - if reminder.RelativeTo != "" { - updatedReminders = append(updatedReminders, reminder) - } - } // append absolute triggers from ReminderDates for _, reminderDate := range reminderDates { - updatedReminders = append(updatedReminders, &TaskReminder{TaskID: t.ID, Reminder: reminderDate}) + reminders = append(reminders, &TaskReminder{TaskID: t.ID, Reminder: reminderDate}) + } + t.Reminders = reminders +} + +// Set the absolute trigger dates for Reminders with relative period +func updateRelativeReminderDates(reminders []*TaskReminder, dueDate time.Time, startDate time.Time, endDate time.Time) { + for _, reminder := range reminders { + relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second + switch reminder.RelativeTo { + case ReminderRelationDueDate: + if !dueDate.IsZero() { + reminder.Reminder = dueDate.Add(relativeDuration) + } + case ReminderRelationStartDate: + if !startDate.IsZero() { + reminder.Reminder = startDate.Add(relativeDuration) + } + case ReminderRelationEndDate: + if !endDate.IsZero() { + reminder.Reminder = endDate.Add(relativeDuration) + } + } } - t.Reminders = updatedReminders } // Removes all old reminders and adds the new ones. This is a lot easier and less buggy than @@ -1535,7 +1553,7 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDa return } - setRelativeReminderDates(reminders, dueDate, startDate, endDate) + updateRelativeReminderDates(reminders, dueDate, startDate, endDate) // Resolve duplicates and sort them reminderMap := make(map[int64]TaskReminder, len(reminders)) @@ -1554,18 +1572,19 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDa RelativePeriod: r.RelativePeriod, RelativeTo: r.RelativeTo} _, err = s.Insert(taskReminder) - t.Reminders = append(t.Reminders, taskReminder) - t.ReminderDates = append(t.ReminderDates, taskReminder.Reminder) if err != nil { return err } + t.Reminders = append(t.Reminders, taskReminder) + t.ReminderDates = append(t.ReminderDates, taskReminder.Reminder) } + // sort reminders sort.Slice(t.Reminders, func(i, j int) bool { return t.Reminders[i].Reminder.Before(t.Reminders[j].Reminder) }) - if len(reminders) == 0 { + if len(t.Reminders) == 0 { t.Reminders = nil t.ReminderDates = nil } @@ -1574,26 +1593,6 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDa return } -func setRelativeReminderDates(reminders []*TaskReminder, dueDate time.Time, startDate time.Time, endDate time.Time) { - for _, reminder := range reminders { - relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second - switch reminder.RelativeTo { - case ReminderDueDate: - if !dueDate.IsZero() { - reminder.Reminder = dueDate.Add(relativeDuration) - } - case ReminderStartDate: - if !startDate.IsZero() { - reminder.Reminder = startDate.Add(relativeDuration) - } - case ReminderEndDate: - if !endDate.IsZero() { - reminder.Reminder = endDate.Add(relativeDuration) - } - } - } -} - func updateTaskLastUpdated(s *xorm.Session, task *Task) error { _, err := s.ID(task.ID).Cols("updated").Update(task) return err diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index 84b7a79cb..4a1e819b0 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -103,9 +103,11 @@ func TestTask_Create(t *testing.T) { assert.NoError(t, err) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder) assert.Equal(t, int64(1), task.Reminders[0].RelativePeriod) - assert.Equal(t, ReminderDueDate, task.Reminders[0].RelativeTo) + assert.Equal(t, ReminderRelationDueDate, task.Reminders[0].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder) + assert.Equal(t, ReminderRelationStartDate, task.Reminders[1].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder) + assert.Equal(t, ReminderRelationEndDate, task.Reminders[2].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder) err = s.Commit() assert.NoError(t, err) @@ -442,9 +444,11 @@ func TestTask_Update(t *testing.T) { assert.NoError(t, err) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 1, 0, time.Local), task.Reminders[0].Reminder) assert.Equal(t, int64(1), task.Reminders[0].RelativePeriod) - assert.Equal(t, ReminderDueDate, task.Reminders[0].RelativeTo) + assert.Equal(t, ReminderRelationDueDate, task.Reminders[0].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 8, 0, time.Local), task.Reminders[1].Reminder) + assert.Equal(t, ReminderRelationStartDate, task.Reminders[1].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 22, 5, 19, 0, time.Local), task.Reminders[2].Reminder) + assert.Equal(t, ReminderRelationEndDate, task.Reminders[2].RelativeTo) assert.Equal(t, time.Date(2023, time.March, 7, 23, 0, 0, 0, time.Local), task.Reminders[3].Reminder) err = s.Commit() assert.NoError(t, err) @@ -474,6 +478,40 @@ func TestTask_Update(t *testing.T) { assert.NoError(t, err) db.AssertCount(t, "task_reminders", builder.Eq{"task_id": 1}, 1) }) + t.Run("update relative reminder when start_date changes", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + // given task with start_date and relative reminder for start_date + taskBefore := &Task{ + Title: "test", + ListID: 1, + StartDate: time.Date(2022, time.March, 8, 8, 5, 20, 0, time.Local), + Reminders: []*TaskReminder{ + { + RelativeTo: "start_date", + RelativePeriod: -60, + }, + }} + err := taskBefore.Create(s, u) + assert.NoError(t, err) + err = s.Commit() + assert.NoError(t, err) + assert.Equal(t, time.Date(2022, time.March, 8, 8, 4, 20, 0, time.Local), taskBefore.Reminders[0].Reminder) + + // when start_date is modified + task := taskBefore + task.StartDate = time.Date(2023, time.March, 8, 8, 5, 0, 0, time.Local) + task.ReminderDates = nil + err = task.Update(s, u) + assert.NoError(t, err) + + // then reminder time is updated + assert.Equal(t, time.Date(2023, time.March, 8, 8, 4, 0, 0, time.Local), task.Reminders[0].Reminder) + err = s.Commit() + assert.NoError(t, err) + }) } func TestTask_Delete(t *testing.T) { diff --git a/pkg/modules/migration/ticktick/ticktick.go b/pkg/modules/migration/ticktick/ticktick.go index 8036ffa88..fd3822a3b 100644 --- a/pkg/modules/migration/ticktick/ticktick.go +++ b/pkg/modules/migration/ticktick/ticktick.go @@ -149,7 +149,7 @@ func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.Namespace if !t.DueDate.IsZero() && t.Reminder > 0 { task.Task.Reminders = []*models.TaskReminder{ { - RelativeTo: models.ReminderDueDate, + RelativeTo: models.ReminderRelationDueDate, RelativePeriod: int64((t.Reminder * -1).Seconds()), }, } diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 728221ea7..54833a08a 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7790,7 +7790,7 @@ const docTemplate = `{ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders", "type": "array", "items": { "type": "string" @@ -8317,9 +8317,9 @@ const docTemplate = `{ "end_date" ], "x-enum-varnames": [ - "ReminderDueDate", - "ReminderStartDate", - "ReminderEndDate" + "ReminderRelationDueDate", + "ReminderRelationStartDate", + "ReminderRelationEndDate" ] }, "models.Right": { @@ -8537,7 +8537,7 @@ const docTemplate = `{ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders", "type": "array", "items": { "type": "string" @@ -8721,7 +8721,7 @@ const docTemplate = `{ "type": "object", "properties": { "relative_period": { - "description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date, positive after.", + "description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date. Default: 0, tiggers when RelativeTo is due.", "type": "integer" }, "relative_to": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 773189696..31b64b40e 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7782,7 +7782,7 @@ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders", "type": "array", "items": { "type": "string" @@ -8309,9 +8309,9 @@ "end_date" ], "x-enum-varnames": [ - "ReminderDueDate", - "ReminderStartDate", - "ReminderEndDate" + "ReminderRelationDueDate", + "ReminderRelationStartDate", + "ReminderRelationEndDate" ] }, "models.Right": { @@ -8529,7 +8529,7 @@ ] }, "reminder_dates": { - "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders[]", + "description": "An array of datetimes when the user wants to be reminded of the task.\n\nDeprecated: Use Reminders", "type": "array", "items": { "type": "string" @@ -8713,7 +8713,7 @@ "type": "object", "properties": { "relative_period": { - "description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date, positive after.", + "description": "A period in seconds relative to another date argument. Negative values mean the reminder triggers before the date. Default: 0, tiggers when RelativeTo is due.", "type": "integer" }, "relative_to": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index a7b1a80e5..8ba6ea3a8 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -199,7 +199,7 @@ definitions: description: |- An array of datetimes when the user wants to be reminded of the task. - Deprecated: Use Reminders[] + Deprecated: Use Reminders items: type: string type: array @@ -609,9 +609,9 @@ definitions: - end_date type: string x-enum-varnames: - - ReminderDueDate - - ReminderStartDate - - ReminderEndDate + - ReminderRelationDueDate + - ReminderRelationStartDate + - ReminderRelationEndDate models.Right: enum: - 0 @@ -782,7 +782,7 @@ definitions: description: |- An array of datetimes when the user wants to be reminded of the task. - Deprecated: Use Reminders[] + Deprecated: Use Reminders items: type: string type: array @@ -915,8 +915,9 @@ definitions: models.TaskReminder: properties: relative_period: - description: A period in seconds relative to another date argument. Negative - values mean the reminder triggers before the date, positive after. + description: 'A period in seconds relative to another date argument. Negative + values mean the reminder triggers before the date. Default: 0, tiggers when + RelativeTo is due.' type: integer relative_to: allOf: -- 2.45.1 From 7f9e70cc1f64c3683d17f330419e7aa0e3557140 Mon Sep 17 00:00:00 2001 From: ce72 Date: Wed, 8 Mar 2023 15:55:11 +0100 Subject: [PATCH 10/24] fix: Review findings --- pkg/models/tasks.go | 47 ++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 9762fb2a8..9f343c8f4 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -973,13 +973,8 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err } } - // Deprecated: the if clause can be removed when ReminderDates will be removed - if t.ReminderDates != nil { - t.overwriteRemindersWithReminderDates(t.ReminderDates) - } - // Update the reminders - if err := t.updateReminders(s, t.Reminders, t.DueDate, t.StartDate, t.EndDate); err != nil { + if err := t.updateReminders(s, t); err != nil { return err } @@ -1031,7 +1026,7 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { t.ListID = ot.ListID } - // Get the reminders + // Get the stored reminders reminders, err := getRemindersForTasks(s, []int64{t.ID}) if err != nil { return @@ -1066,13 +1061,8 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { return err } - // Deprecated: This statement can be removed when ReminderDates will be removed - if t.ReminderDates != nil { - t.overwriteRemindersWithReminderDates(t.ReminderDates) - } - // Update the reminders - if err := ot.updateReminders(s, t.Reminders, t.DueDate, t.StartDate, t.EndDate); err != nil { + if err := ot.updateReminders(s, t); err != nil { return err } @@ -1520,21 +1510,21 @@ func (t *Task) overwriteRemindersWithReminderDates(reminderDates []time.Time) { } // Set the absolute trigger dates for Reminders with relative period -func updateRelativeReminderDates(reminders []*TaskReminder, dueDate time.Time, startDate time.Time, endDate time.Time) { - for _, reminder := range reminders { +func updateRelativeReminderDates(task *Task) { + for _, reminder := range task.Reminders { relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second switch reminder.RelativeTo { case ReminderRelationDueDate: - if !dueDate.IsZero() { - reminder.Reminder = dueDate.Add(relativeDuration) + if !task.DueDate.IsZero() { + reminder.Reminder = task.DueDate.Add(relativeDuration) } case ReminderRelationStartDate: - if !startDate.IsZero() { - reminder.Reminder = startDate.Add(relativeDuration) + if !task.StartDate.IsZero() { + reminder.Reminder = task.StartDate.Add(relativeDuration) } case ReminderRelationEndDate: - if !endDate.IsZero() { - reminder.Reminder = endDate.Add(relativeDuration) + if !task.EndDate.IsZero() { + reminder.Reminder = task.EndDate.Add(relativeDuration) } } } @@ -1544,7 +1534,12 @@ func updateRelativeReminderDates(reminders []*TaskReminder, dueDate time.Time, s // trying to figure out which reminders changed and then only re-add those needed. And since it does // not make a performance difference we'll just do that. // The parameter is a slice which holds the new reminders. -func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDate time.Time, startDate time.Time, endDate time.Time) (err error) { +func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) { + + // Deprecated: This statement must be removed when ReminderDates will be removed + if t.ReminderDates != nil { + t.overwriteRemindersWithReminderDates(t.ReminderDates) + } _, err = s. Where("task_id = ?", t.ID). @@ -1553,12 +1548,12 @@ func (t *Task) updateReminders(s *xorm.Session, reminders []*TaskReminder, dueDa return } - updateRelativeReminderDates(reminders, dueDate, startDate, endDate) + updateRelativeReminderDates(task) // Resolve duplicates and sort them - reminderMap := make(map[int64]TaskReminder, len(reminders)) - for _, reminder := range reminders { - reminderMap[reminder.Reminder.UTC().Unix()] = *reminder + reminderMap := make(map[int64]*TaskReminder, len(task.Reminders)) + for _, reminder := range task.Reminders { + reminderMap[reminder.Reminder.UTC().Unix()] = reminder } t.Reminders = make([]*TaskReminder, 0, len(reminderMap)) -- 2.45.1 From 26119d585bf8720ec8cbffd4c8a8153cb46033f0 Mon Sep 17 00:00:00 2001 From: ce72 Date: Wed, 8 Mar 2023 23:10:38 +0100 Subject: [PATCH 11/24] fix: update correct task variable --- pkg/models/tasks.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 9f343c8f4..e60750e2b 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1537,8 +1537,8 @@ func updateRelativeReminderDates(task *Task) { func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) { // Deprecated: This statement must be removed when ReminderDates will be removed - if t.ReminderDates != nil { - t.overwriteRemindersWithReminderDates(t.ReminderDates) + if task.ReminderDates != nil { + task.overwriteRemindersWithReminderDates(task.ReminderDates) } _, err = s. -- 2.45.1 From e089976d95d247cfd26e4ed3d74b3d2a57307069 Mon Sep 17 00:00:00 2001 From: ce72 Date: Mon, 13 Mar 2023 07:14:54 +0100 Subject: [PATCH 12/24] feat: add validation for relative reminders --- pkg/models/error.go | 27 +++++++++++++++++++++++++++ pkg/models/tasks.go | 15 +++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/pkg/models/error.go b/pkg/models/error.go index 580ba0551..c1a2e0624 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -875,6 +875,33 @@ func (err ErrUserAlreadyAssigned) HTTPError() web.HTTPError { } } +// ErrReminderRelativeToMissing represents an error where a task has a relative reminder without reference date +type ErrReminderRelativeToMissing struct { + TaskID int64 +} + +// IsErrReminderRelativeToMissing checks if an error is ErrReminderRelativeToMissing. +func IsErrReminderRelativeToMissing(err error) bool { + _, ok := err.(ErrReminderRelativeToMissing) + return ok +} + +func (err ErrReminderRelativeToMissing) Error() string { + return fmt.Sprintf("Task [TaskID: %v] has a relative reminder without relative_to", err.TaskID) +} + +// ErrCodeRelationDoesNotExist holds the unique world-error code of this error +const ErrCodeReminderRelativeToMissing = 4022 + +// HTTPError holds the http error description +func (err ErrReminderRelativeToMissing) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusBadRequest, + Code: ErrCodeReminderRelativeToMissing, + Message: "Relative reminder without relative_to", + } +} + // ================= // Namespace errors // ================= diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index e60750e2b..f4bda048c 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1510,7 +1510,7 @@ func (t *Task) overwriteRemindersWithReminderDates(reminderDates []time.Time) { } // Set the absolute trigger dates for Reminders with relative period -func updateRelativeReminderDates(task *Task) { +func updateRelativeReminderDates(task *Task) (err error) { for _, reminder := range task.Reminders { relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second switch reminder.RelativeTo { @@ -1526,8 +1526,16 @@ func updateRelativeReminderDates(task *Task) { if !task.EndDate.IsZero() { reminder.Reminder = task.EndDate.Add(relativeDuration) } + default: + if reminder.RelativePeriod != 0 { + err = ErrReminderRelativeToMissing{ + TaskID: task.ID, + } + } + return err } } + return nil } // Removes all old reminders and adds the new ones. This is a lot easier and less buggy than @@ -1548,7 +1556,10 @@ func (t *Task) updateReminders(s *xorm.Session, task *Task) (err error) { return } - updateRelativeReminderDates(task) + err = updateRelativeReminderDates(task) + if err != nil { + return + } // Resolve duplicates and sort them reminderMap := make(map[int64]*TaskReminder, len(task.Reminders)) -- 2.45.1 From 8098d66727bb133856805cd4e724eea97dc6e167 Mon Sep 17 00:00:00 2001 From: ce72 Date: Wed, 15 Mar 2023 15:15:51 +0100 Subject: [PATCH 13/24] fix: update all relative reminders --- pkg/models/tasks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 48f76b2d1..97e8dcc39 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1531,8 +1531,8 @@ func updateRelativeReminderDates(task *Task) (err error) { err = ErrReminderRelativeToMissing{ TaskID: task.ID, } + return err } - return err } } return nil -- 2.45.1 From 7fe3a1ade7a5d2e9e4c0800f4a9947a6975a0194 Mon Sep 17 00:00:00 2001 From: ce72 Date: Wed, 15 Mar 2023 23:03:37 +0100 Subject: [PATCH 14/24] fix: unset reminder date if referenced date is missing --- pkg/models/tasks.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 97e8dcc39..f2471b1ac 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1517,14 +1517,20 @@ func updateRelativeReminderDates(task *Task) (err error) { case ReminderRelationDueDate: if !task.DueDate.IsZero() { reminder.Reminder = task.DueDate.Add(relativeDuration) + } else { + reminder.Reminder = time.Time{} } case ReminderRelationStartDate: if !task.StartDate.IsZero() { reminder.Reminder = task.StartDate.Add(relativeDuration) + } else { + reminder.Reminder = time.Time{} } case ReminderRelationEndDate: if !task.EndDate.IsZero() { reminder.Reminder = task.EndDate.Add(relativeDuration) + } else { + reminder.Reminder = time.Time{} } default: if reminder.RelativePeriod != 0 { -- 2.45.1 From f695042465328aae0a84cadd2a1da29d5d80524c Mon Sep 17 00:00:00 2001 From: ce72 Date: Fri, 3 Mar 2023 13:49:40 +0100 Subject: [PATCH 15/24] feat(caldav): Sync Reminders / VALARM Relates to #1408 --- docs/content/doc/usage/caldav.md | 21 ++++--- pkg/caldav/caldav.go | 26 ++++---- pkg/caldav/caldav_test.go | 48 +++++++++++++++ pkg/caldav/parsing.go | 99 +++++++++++++++++++++++++++++- pkg/caldav/parsing_test.go | 65 +++++++++++++++++++- pkg/db/fixtures/task_reminders.yml | 4 ++ pkg/integrations/caldav_test.go | 8 +++ 7 files changed, 244 insertions(+), 27 deletions(-) diff --git a/docs/content/doc/usage/caldav.md b/docs/content/doc/usage/caldav.md index e43ba0436..60d261299 100644 --- a/docs/content/doc/usage/caldav.md +++ b/docs/content/doc/usage/caldav.md @@ -39,30 +39,31 @@ Vikunja currently supports the following properties: * `PRIORITY` * `CATEGORIES` * `COMPLETED` +* `CREATED` (only Vikunja -> Client) * `DUE` -* `DTSTART` * `DURATION` -* `ORGANIZER` -* `RELATED-TO` -* `CREATED` * `DTSTAMP` -* `LAST-MODIFIED` -* Recurrence +* `DTSTART` +* `LAST-MODIFIED` (only Vikunja -> Client) +* `RRULE` (Recurrence) (only Vikunja -> Client) +* `VALARM` (Reminders) Vikunja **currently does not** support these properties: * `ATTACH` * `CLASS` * `COMMENT` +* `CONTACT` * `GEO` * `LOCATION` +* `ORGANIZER` (disabled) * `PERCENT-COMPLETE` -* `RESOURCES` -* `STATUS` -* `CONTACT` * `RECURRENCE-ID` -* `URL` +* `RELATED-TO` +* `RESOURCES` * `SEQUENCE` +* `STATUS` +* `URL` ## Tested Clients diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index 0f324f1c8..f04eff123 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -31,19 +31,6 @@ import ( // DateFormat is the caldav date format 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 type Todo struct { // Required @@ -65,6 +52,7 @@ type Todo struct { Duration time.Duration RepeatAfter int64 RepeatMode models.TaskRepeatMode + Alarms []Alarm Created time.Time Updated time.Time // last-mod @@ -247,6 +235,18 @@ CATEGORIES:` + strings.Join(t.Categories, ",") caldavtodos += ` LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated) + for _, a := range t.Alarms { + if a.Description == "" { + a.Description = t.Summary + } + + caldavtodos += ` +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME:` + makeCalDavTimeFromTimeStamp(a.Time) + ` +ACTION:DISPLAY +DESCRIPTION:` + a.Description + ` +END:VALARM` + } caldavtodos += ` END:VTODO` } diff --git a/pkg/caldav/caldav_test.go b/pkg/caldav/caldav_test.go index a8da6e240..11b1a42cd 100644 --- a/pkg/caldav/caldav_test.go +++ b/pkg/caldav/caldav_test.go @@ -520,6 +520,54 @@ X-FUNAMBOL-COLOR:#affffeFF CATEGORIES:label1,label2 LAST-MODIFIED:00010101T000000Z 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", + }, + }, + }, + }, + }, + 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 +END:VTODO END:VCALENDAR`, }, } diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 4b887f86b..3ab4a578e 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -17,6 +17,8 @@ package caldav import ( + "errors" + "regexp" "strconv" "strings" "time" @@ -38,6 +40,12 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT for _, label := range t.Labels { categories = append(categories, label.Title) } + var alarms []Alarm + for _, reminder := range t.Reminders { + alarms = append(alarms, Alarm{ + Time: reminder, + }) + } caldavtodos = append(caldavtodos, &Todo{ Timestamp: t.Updated, @@ -56,6 +64,7 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT Duration: duration, RepeatAfter: t.RepeatAfter, RepeatMode: t.RepeatMode, + Alarms: alarms, }) } @@ -72,10 +81,13 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { if err != nil { return nil, err } - - // We put the task details in a map to be able to handle them more easily + vTodo, ok := parsed.Components[0].(*ics.VTodo) + 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) - for _, c := range parsed.Components[0].UnknownPropertiesIANAProperties() { + for _, c := range vTodo.UnknownPropertiesIANAProperties() { task[c.IANAToken] = c.Value } @@ -127,9 +139,55 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { vTask.EndDate = vTask.StartDate.Add(duration) } + for _, vAlarm := range vTodo.SubComponents() { + switch vAlarm := vAlarm.(type) { + case *ics.VAlarm: + parseVAlarm(vAlarm, vTask) + } + } + return } +func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) { + for _, property := range vAlarm.UnknownPropertiesIANAProperties() { + if property.IANAToken == "TRIGGER" { + if len(property.ICalParameters["VALUE"]) > 0 { + switch property.ICalParameters["VALUE"][0] { + case "DATE-TIME": + // TRIGGER;VALUE=DATE-TIME:20181201T011210Z + vTask.Reminders = append(vTask.Reminders, caldavTimeToTimestamp(property.Value)) + } + } else if len(property.ICalParameters["RELATED"]) > 0 { + duration := parseDuration(property.Value) + switch property.ICalParameters["RELATED"][0] { + case "START": + // TRIGGER;RELATED=START:-P2D + if !vTask.StartDate.IsZero() { + vTask.Reminders = append(vTask.Reminders, vTask.StartDate.Add(duration)) + } + case "END": + // TRIGGER;RELATED=END:-P2D + if !vTask.EndDate.IsZero() { + vTask.Reminders = append(vTask.Reminders, vTask.EndDate.Add(duration)) + } else if !vTask.DueDate.IsZero() { + vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) + } + } + } else { + duration := parseDuration(property.Value) + if duration != 0 { + // TRIGGER:-PT60M + if !vTask.DueDate.IsZero() { + vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) + } + } + + } + } + } +} + // https://tools.ietf.org/html/rfc5545#section-3.3.5 func caldavTimeToTimestamp(tstring string) time.Time { if tstring == "" { @@ -153,3 +211,38 @@ func caldavTimeToTimestamp(tstring string) time.Time { } return t } + +// From https://stackoverflow.com/a/57617885; with support for negative durations added +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[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 +} + +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 +} diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index f5923b60c..c77f01fc1 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -118,6 +118,55 @@ END:VCALENDAR`, Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()), }, }, + { + name: "With alarm", + 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;VALUE=DATE-TIME:20181201T011210Z +ACTION:DISPLAY +END:VALARM +BEGIN:VALARM +TRIGGER:-PT60M +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: []time.Time{ + time.Date(2018, 12, 1, 1, 12, 10, 0, config.GetTimeZone()), + time.Date(2023, 3, 4, 14, 0, 0, 0, config.GetTimeZone()), + time.Date(2023, 2, 27, 17, 00, 0, 0, config.GetTimeZone()), + time.Date(2023, 3, 4, 14, 30, 0, 0, config.GetTimeZone()), + }, + Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -127,7 +176,7 @@ END:VCALENDAR`, return } 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 +224,10 @@ func TestGetCaldavTodosForTasks(t *testing.T) { Title: "label2", }, }, + Reminders: []time.Time{ + time.Unix(1543626730, 0).In(config.GetTimeZone()), + time.Unix(1543626731, 0).In(config.GetTimeZone()), + }, }, }, }, @@ -200,6 +253,16 @@ PRIORITY:3 RRULE:FREQ=SECONDLY;INTERVAL=86400 CATEGORIES:label1,label2 LAST-MODIFIED:20181201T011205Z +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME:20181201T011210Z +ACTION:DISPLAY +DESCRIPTION:Task 1 +END:VALARM +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME:20181201T011211Z +ACTION:DISPLAY +DESCRIPTION:Task 1 +END:VALARM END:VTODO END:VCALENDAR`, }, diff --git a/pkg/db/fixtures/task_reminders.yml b/pkg/db/fixtures/task_reminders.yml index d3d023e9b..a5e5ec07a 100644 --- a/pkg/db/fixtures/task_reminders.yml +++ b/pkg/db/fixtures/task_reminders.yml @@ -12,3 +12,7 @@ task_id: 2 reminder: 2018-12-01 01:13:44 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 diff --git a/pkg/integrations/caldav_test.go b/pkg/integrations/caldav_test.go index 5036a305c..7ab704446 100644 --- a/pkg/integrations/caldav_test.go +++ b/pkg/integrations/caldav_test.go @@ -37,6 +37,10 @@ SUMMARY:Caldav Task 1 CATEGORIES:tag1,tag2,tag3 CREATED:20230301T073337Z LAST-MODIFIED:20230301T073337Z +BEGIN:VALARM +TRIGGER;VALUE=DATE-TIME:20230304T150000Z +ACTION:DISPLAY +END:VALARM END:VTODO 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(), "PRIORITY:3") 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") }) } -- 2.45.1 From 30c1d698e9eaebb065baa4163fe104b043cbca0b Mon Sep 17 00:00:00 2001 From: ce72 Date: Sat, 4 Mar 2023 12:36:02 +0100 Subject: [PATCH 16/24] chore(caldav): cleanup --- pkg/caldav/caldav.go | 52 ------- pkg/caldav/caldav_test.go | 277 +------------------------------------ pkg/caldav/parsing.go | 57 ++++---- pkg/caldav/parsing_test.go | 65 ++++++--- 4 files changed, 79 insertions(+), 372 deletions(-) diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index f04eff123..7ec629218 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -88,58 +88,6 @@ X-OUTLOOK-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 { seconds := duration.Seconds() - duration.Minutes()*60 minutes := duration.Minutes() - duration.Hours()*60 diff --git a/pkg/caldav/caldav_test.go b/pkg/caldav/caldav_test.go index 11b1a42cd..63252fbb1 100644 --- a/pkg/caldav/caldav_test.go +++ b/pkg/caldav/caldav_test.go @@ -26,275 +26,6 @@ import ( "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) { type args struct { config *Config @@ -531,15 +262,15 @@ END:VCALENDAR`, }, todos: []*Todo{ { - Summary: "Todo #1", - UID: "randommduid", - Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()), + 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()), + Time: time.Unix(1543626724, 0).In(config.GetTimeZone()), Description: "alarm description", }, }, diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 3ab4a578e..e1fb0c289 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -155,34 +155,37 @@ func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) { if len(property.ICalParameters["VALUE"]) > 0 { switch property.ICalParameters["VALUE"][0] { case "DATE-TIME": - // TRIGGER;VALUE=DATE-TIME:20181201T011210Z + // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z vTask.Reminders = append(vTask.Reminders, caldavTimeToTimestamp(property.Value)) } - } else if len(property.ICalParameters["RELATED"]) > 0 { - duration := parseDuration(property.Value) - switch property.ICalParameters["RELATED"][0] { - case "START": - // TRIGGER;RELATED=START:-P2D - if !vTask.StartDate.IsZero() { - vTask.Reminders = append(vTask.Reminders, vTask.StartDate.Add(duration)) - } - case "END": - // TRIGGER;RELATED=END:-P2D - if !vTask.EndDate.IsZero() { - vTask.Reminders = append(vTask.Reminders, vTask.EndDate.Add(duration)) - } else if !vTask.DueDate.IsZero() { - vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) - } - } - } else { - duration := parseDuration(property.Value) - if duration != 0 { - // TRIGGER:-PT60M - if !vTask.DueDate.IsZero() { - vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) - } - } - + // At the moment I don't think this should be merged. Relative triggers cannot + // be stored properly. This would result in missing or duplicate alarms on the + // client. + // + // } else if len(property.ICalParameters["RELATED"]) > 0 { + // duration := parseDuration(property.Value) + // switch property.ICalParameters["RELATED"][0] { + // case "START": + // // Example: TRIGGER;RELATED=START:-P2D + // if !vTask.StartDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.StartDate.Add(duration)) + // } + // case "END": + // // Example: TRIGGER;RELATED=END:-P2D + // if !vTask.EndDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.EndDate.Add(duration)) + // } else if !vTask.DueDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) + // } + // } + // } else { + // duration := parseDuration(property.Value) + // if duration != 0 { + // // Example: TRIGGER:-PT60M + // if !vTask.DueDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) + // } + // } } } } @@ -212,7 +215,7 @@ func caldavTimeToTimestamp(tstring string) time.Time { return t } -// From https://stackoverflow.com/a/57617885; with support for negative durations added +// TODO: move to utils/time.go and share with ticktick.go 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 diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index c77f01fc1..1755e6ea0 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -119,7 +119,7 @@ END:VCALENDAR`, }, }, { - name: "With alarm", + name: "With alarm (time trigger)", args: args{content: `BEGIN:VCALENDAR VERSION:2.0 METHOD:PUBLISH @@ -131,24 +131,10 @@ UID:randomuid DTSTAMP:20181201T011204 SUMMARY:Todo #1 DESCRIPTION:Lorem Ipsum -DTSTART:20230228T170000Z -DUE:20230304T150000Z BEGIN:VALARM TRIGGER;VALUE=DATE-TIME:20181201T011210Z ACTION:DISPLAY END:VALARM -BEGIN:VALARM -TRIGGER:-PT60M -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`, }, @@ -156,17 +142,56 @@ END:VCALENDAR`, 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: []time.Time{ time.Date(2018, 12, 1, 1, 12, 10, 0, config.GetTimeZone()), - time.Date(2023, 3, 4, 14, 0, 0, 0, config.GetTimeZone()), - time.Date(2023, 2, 27, 17, 00, 0, 0, config.GetTimeZone()), - time.Date(2023, 3, 4, 14, 30, 0, 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:-PT60M +// 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: []time.Time{ +// time.Date(2023, 3, 4, 14, 0, 0, 0, config.GetTimeZone()), +// time.Date(2023, 2, 27, 17, 00, 0, 0, config.GetTimeZone()), +// time.Date(2023, 3, 4, 14, 30, 0, 0, config.GetTimeZone()), +// }, +// Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()), +// }, +// }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { -- 2.45.1 From 06508f02873112fb064b41121ef5f3734c6a7ed9 Mon Sep 17 00:00:00 2001 From: ce72 Date: Thu, 16 Mar 2023 17:10:21 +0100 Subject: [PATCH 17/24] feat: convert Reminders to VALARMs --- pkg/caldav/caldav.go | 55 +++++++++++++++----- pkg/caldav/caldav_test.go | 29 ++++++++++- pkg/caldav/parsing.go | 58 ++++++++++----------- pkg/caldav/parsing_test.go | 104 ++++++++++++++++++++----------------- 4 files changed, 153 insertions(+), 93 deletions(-) diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index 7ec629218..900c051f2 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -61,6 +61,8 @@ type Todo struct { // Alarm holds infos about an alarm from a caldav event type Alarm struct { Time time.Time + Duration time.Duration + RelativeTo string Description string } @@ -182,19 +184,7 @@ CATEGORIES:` + strings.Join(t.Categories, ",") caldavtodos += ` LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated) - - for _, a := range t.Alarms { - if a.Description == "" { - a.Description = t.Summary - } - - caldavtodos += ` -BEGIN:VALARM -TRIGGER;VALUE=DATE-TIME:` + makeCalDavTimeFromTimeStamp(a.Time) + ` -ACTION:DISPLAY -DESCRIPTION:` + a.Description + ` -END:VALARM` - } + caldavtodos += ParseAlarms(t.Alarms, t.Summary) caldavtodos += ` END:VTODO` } @@ -205,10 +195,49 @@ END:VCALENDAR` // Need a line break 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 "due_date": + caldavalarms += ` +TRIGGER:` + makeCalDavDuration(a.Duration) + case "start_date": + caldavalarms += ` +TRIGGER;RELATED=START:` + makeCalDavDuration(a.Duration) + case "end_date": + 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) { return ts.In(time.UTC).Format(DateFormat) + "Z" } +func makeCalDavDuration(duration time.Duration) (caldavtime string) { + if duration < 0 { + duration = duration.Abs() + caldavtime = "-" + } + caldavtime += "PT" + strings.ToUpper(duration.Truncate(time.Millisecond).String()) + return +} + func calcAlarmDateFromReminder(eventStart, reminder time.Time) (alarmTime string) { diff := reminder.Sub(eventStart) diffStr := strings.ToUpper(diff.String()) diff --git a/pkg/caldav/caldav_test.go b/pkg/caldav/caldav_test.go index 63252fbb1..efaccd359 100644 --- a/pkg/caldav/caldav_test.go +++ b/pkg/caldav/caldav_test.go @@ -273,6 +273,18 @@ END:VCALENDAR`, 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", + }, }, }, }, @@ -298,6 +310,21 @@ TRIGGER;VALUE=DATE-TIME:20181201T011204Z ACTION:DISPLAY DESCRIPTION:alarm description END:VALARM +BEGIN:VALARM +TRIGGER:-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`, }, @@ -305,7 +332,7 @@ END:VCALENDAR`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotCaldavtasks := ParseTodos(tt.args.config, tt.args.todos) - assert.Equal(t, gotCaldavtasks, tt.wantCaldavtasks) + assert.Equal(t, tt.wantCaldavtasks, gotCaldavtasks) }) } } diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index e1fb0c289..938287123 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -43,7 +43,9 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT var alarms []Alarm for _, reminder := range t.Reminders { alarms = append(alarms, Alarm{ - Time: reminder, + Time: reminder.Reminder, + Duration: time.Duration(reminder.RelativePeriod) * time.Second, + RelativeTo: string(reminder.RelativeTo), }) } @@ -156,36 +158,32 @@ func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) { switch property.ICalParameters["VALUE"][0] { case "DATE-TIME": // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z - vTask.Reminders = append(vTask.Reminders, caldavTimeToTimestamp(property.Value)) + vTask.ReminderDates = append(vTask.ReminderDates, caldavTimeToTimestamp(property.Value)) } - // At the moment I don't think this should be merged. Relative triggers cannot - // be stored properly. This would result in missing or duplicate alarms on the - // client. - // - // } else if len(property.ICalParameters["RELATED"]) > 0 { - // duration := parseDuration(property.Value) - // switch property.ICalParameters["RELATED"][0] { - // case "START": - // // Example: TRIGGER;RELATED=START:-P2D - // if !vTask.StartDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.StartDate.Add(duration)) - // } - // case "END": - // // Example: TRIGGER;RELATED=END:-P2D - // if !vTask.EndDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.EndDate.Add(duration)) - // } else if !vTask.DueDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) - // } - // } - // } else { - // duration := parseDuration(property.Value) - // if duration != 0 { - // // Example: TRIGGER:-PT60M - // if !vTask.DueDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) - // } - // } + //} else if len(property.ICalParameters["RELATED"]) > 0 { + // duration := parseDuration(property.Value) + // switch property.ICalParameters["RELATED"][0] { + // case "START": + // // Example: TRIGGER;RELATED=START:-P2D + // if !vTask.StartDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.StartDate.Add(duration)) + // } + // case "END": + // // Example: TRIGGER;RELATED=END:-P2D + // if !vTask.EndDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.EndDate.Add(duration)) + // } else if !vTask.DueDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) + // } + // } + //} else { + // duration := parseDuration(property.Value) + // if duration != 0 { + // // Example: TRIGGER:-PT60M + // if !vTask.DueDate.IsZero() { + // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) + // } + // } } } } diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index 1755e6ea0..164dd0e77 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -142,56 +142,56 @@ END:VCALENDAR`, Title: "Todo #1", UID: "randomuid", Description: "Lorem Ipsum", - Reminders: []time.Time{ + ReminderDates: []time.Time{ 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:-PT60M -// 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: []time.Time{ -// time.Date(2023, 3, 4, 14, 0, 0, 0, config.GetTimeZone()), -// time.Date(2023, 2, 27, 17, 00, 0, 0, config.GetTimeZone()), -// time.Date(2023, 3, 4, 14, 30, 0, 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:-PT60M + // 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: []time.Time{ + // time.Date(2023, 3, 4, 14, 0, 0, 0, config.GetTimeZone()), + // time.Date(2023, 2, 27, 17, 00, 0, 0, config.GetTimeZone()), + // time.Date(2023, 3, 4, 14, 30, 0, 0, config.GetTimeZone()), + // }, + // Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()), + // }, + // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -249,9 +249,15 @@ func TestGetCaldavTodosForTasks(t *testing.T) { Title: "label2", }, }, - Reminders: []time.Time{ - time.Unix(1543626730, 0).In(config.GetTimeZone()), - time.Unix(1543626731, 0).In(config.GetTimeZone()), + Reminders: []*models.TaskReminder{ + { + Reminder: time.Unix(1543626730, 0).In(config.GetTimeZone()), + }, + { + Reminder: time.Unix(1543626731, 0).In(config.GetTimeZone()), + RelativePeriod: -3600, + RelativeTo: models.ReminderRelationDueDate, + }, }, }, }, @@ -284,7 +290,7 @@ ACTION:DISPLAY DESCRIPTION:Task 1 END:VALARM BEGIN:VALARM -TRIGGER;VALUE=DATE-TIME:20181201T011211Z +TRIGGER:-PT1H0M0S ACTION:DISPLAY DESCRIPTION:Task 1 END:VALARM -- 2.45.1 From 32cb200f87c08efeae6c68c5a1a47a14019e5bbb Mon Sep 17 00:00:00 2001 From: ce72 Date: Thu, 16 Mar 2023 21:28:45 +0100 Subject: [PATCH 18/24] feat: convert VALARMs to Reminders --- pkg/caldav/caldav.go | 13 ----- pkg/caldav/parsing.go | 59 +++++++++++---------- pkg/caldav/parsing_test.go | 103 ++++++++++++++++++++----------------- 3 files changed, 88 insertions(+), 87 deletions(-) diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index 900c051f2..830c73f94 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -237,16 +237,3 @@ func makeCalDavDuration(duration time.Duration) (caldavtime string) { caldavtime += "PT" + strings.ToUpper(duration.Truncate(time.Millisecond).String()) return } - -func calcAlarmDateFromReminder(eventStart, reminder time.Time) (alarmTime string) { - diff := reminder.Sub(eventStart) - diffStr := strings.ToUpper(diff.String()) - if diff < 0 { - 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 - return -} diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 938287123..fcd4342d5 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -141,52 +141,56 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { vTask.EndDate = vTask.StartDate.Add(duration) } + reminders := make([]*models.TaskReminder, 0) for _, vAlarm := range vTodo.SubComponents() { switch vAlarm := vAlarm.(type) { case *ics.VAlarm: - parseVAlarm(vAlarm, vTask) + reminders = parseVAlarm(vAlarm, reminders) } } + if len(reminders) > 0 { + vTask.Reminders = reminders + } return } -func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) { +func parseVAlarm(vAlarm *ics.VAlarm, reminders []*models.TaskReminder) []*models.TaskReminder { for _, property := range vAlarm.UnknownPropertiesIANAProperties() { if property.IANAToken == "TRIGGER" { if len(property.ICalParameters["VALUE"]) > 0 { switch property.ICalParameters["VALUE"][0] { case "DATE-TIME": // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z - vTask.ReminderDates = append(vTask.ReminderDates, caldavTimeToTimestamp(property.Value)) + reminders = append(reminders, &models.TaskReminder{ + Reminder: caldavTimeToTimestamp(property.Value)}) + } + } else if len(property.ICalParameters["RELATED"]) > 0 { + duration := parseDuration(property.Value) + switch property.ICalParameters["RELATED"][0] { + case "START": + // Example: TRIGGER;RELATED=START:-P2D + reminders = append(reminders, &models.TaskReminder{ + RelativePeriod: int64(duration.Seconds()), + RelativeTo: models.ReminderRelationStartDate}) + case "END": + // Example: TRIGGER;RELATED=END:-P2D + reminders = append(reminders, &models.TaskReminder{ + RelativePeriod: int64(duration.Seconds()), + RelativeTo: models.ReminderRelationEndDate}) + } + } else { + duration := parseDuration(property.Value) + if duration != 0 { + // Example: TRIGGER:-PT60M + reminders = append(reminders, &models.TaskReminder{ + RelativePeriod: int64(duration.Seconds()), + RelativeTo: models.ReminderRelationDueDate}) } - //} else if len(property.ICalParameters["RELATED"]) > 0 { - // duration := parseDuration(property.Value) - // switch property.ICalParameters["RELATED"][0] { - // case "START": - // // Example: TRIGGER;RELATED=START:-P2D - // if !vTask.StartDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.StartDate.Add(duration)) - // } - // case "END": - // // Example: TRIGGER;RELATED=END:-P2D - // if !vTask.EndDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.EndDate.Add(duration)) - // } else if !vTask.DueDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) - // } - // } - //} else { - // duration := parseDuration(property.Value) - // if duration != 0 { - // // Example: TRIGGER:-PT60M - // if !vTask.DueDate.IsZero() { - // vTask.Reminders = append(vTask.Reminders, vTask.DueDate.Add(duration)) - // } - // } } } } + return reminders } // https://tools.ietf.org/html/rfc5545#section-3.3.5 @@ -213,7 +217,6 @@ func caldavTimeToTimestamp(tstring string) time.Time { return t } -// TODO: move to utils/time.go and share with ticktick.go 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 diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index 164dd0e77..a8c30d510 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -142,56 +142,67 @@ END:VCALENDAR`, Title: "Todo #1", UID: "randomuid", Description: "Lorem Ipsum", - ReminderDates: []time.Time{ - time.Date(2018, 12, 1, 1, 12, 10, 0, config.GetTimeZone()), + 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:-PT60M +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.ReminderRelationDueDate, + RelativePeriod: -3600, + }, + { + RelativeTo: models.ReminderRelationStartDate, + RelativePeriod: -86400, + }, + { + RelativeTo: models.ReminderRelationEndDate, + RelativePeriod: -1800, + }, }, 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:-PT60M - // 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: []time.Time{ - // time.Date(2023, 3, 4, 14, 0, 0, 0, config.GetTimeZone()), - // time.Date(2023, 2, 27, 17, 00, 0, 0, config.GetTimeZone()), - // time.Date(2023, 3, 4, 14, 30, 0, 0, config.GetTimeZone()), - // }, - // Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()), - // }, - // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { -- 2.45.1 From 4018bd6078cceb8c0fb144ac77ded07572bb4ecf Mon Sep 17 00:00:00 2001 From: ce72 Date: Fri, 17 Mar 2023 08:23:52 +0100 Subject: [PATCH 19/24] fix: lint errors --- pkg/caldav/parsing.go | 23 ++++++++++------------- pkg/caldav/parsing_test.go | 8 ++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index fcd4342d5..38a4d0238 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -143,8 +143,7 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { reminders := make([]*models.TaskReminder, 0) for _, vAlarm := range vTodo.SubComponents() { - switch vAlarm := vAlarm.(type) { - case *ics.VAlarm: + if vAlarm, ok := vAlarm.(*ics.VAlarm); ok { reminders = parseVAlarm(vAlarm, reminders) } } @@ -158,14 +157,14 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { func parseVAlarm(vAlarm *ics.VAlarm, reminders []*models.TaskReminder) []*models.TaskReminder { for _, property := range vAlarm.UnknownPropertiesIANAProperties() { if property.IANAToken == "TRIGGER" { - if len(property.ICalParameters["VALUE"]) > 0 { - switch property.ICalParameters["VALUE"][0] { - case "DATE-TIME": + switch { + case len(property.ICalParameters["VALUE"]) > 0: + if property.ICalParameters["VALUE"][0] == "DATE-TIME" { // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z reminders = append(reminders, &models.TaskReminder{ Reminder: caldavTimeToTimestamp(property.Value)}) } - } else if len(property.ICalParameters["RELATED"]) > 0 { + case len(property.ICalParameters["RELATED"]) > 0: duration := parseDuration(property.Value) switch property.ICalParameters["RELATED"][0] { case "START": @@ -179,14 +178,12 @@ func parseVAlarm(vAlarm *ics.VAlarm, reminders []*models.TaskReminder) []*models RelativePeriod: int64(duration.Seconds()), RelativeTo: models.ReminderRelationEndDate}) } - } else { + default: duration := parseDuration(property.Value) - if duration != 0 { - // Example: TRIGGER:-PT60M - reminders = append(reminders, &models.TaskReminder{ - RelativePeriod: int64(duration.Seconds()), - RelativeTo: models.ReminderRelationDueDate}) - } + // Example: TRIGGER:-PT60M + reminders = append(reminders, &models.TaskReminder{ + RelativePeriod: int64(duration.Seconds()), + RelativeTo: models.ReminderRelationDueDate}) } } } diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index a8c30d510..31709a7cc 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -166,6 +166,10 @@ DESCRIPTION:Lorem Ipsum DTSTART:20230228T170000Z DUE:20230304T150000Z BEGIN:VALARM +TRIGGER:PT0S +ACTION:DISPLAY +END:VALARM +BEGIN:VALARM TRIGGER:-PT60M ACTION:DISPLAY END:VALARM @@ -187,6 +191,10 @@ END:VCALENDAR`, 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.ReminderRelationDueDate, + RelativePeriod: 0, + }, { RelativeTo: models.ReminderRelationDueDate, RelativePeriod: -3600, -- 2.45.1 From 6abd212c57c1286f2d9b80663c1f6d4dd7372d29 Mon Sep 17 00:00:00 2001 From: ce72 Date: Fri, 24 Mar 2023 12:37:07 +0100 Subject: [PATCH 20/24] fix(caldav): Do not create label if it exists by title Resolves https://kolaente.dev/vikunja/api/issues/1435 --- pkg/routes/caldav/listStorageProvider.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/routes/caldav/listStorageProvider.go b/pkg/routes/caldav/listStorageProvider.go index 88841670f..57468cd93 100644 --- a/pkg/routes/caldav/listStorageProvider.go +++ b/pkg/routes/caldav/listStorageProvider.go @@ -411,13 +411,13 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod return err } - labelMap := make(map[int64]*models.Label) + labelMap := make(map[string]*models.Label) for _, l := range existingLabels { - labelMap[l.ID] = &l.Label + labelMap[l.Title] = &l.Label } for _, label := range labels { - if l, has := labelMap[label.ID]; has { + if l, has := labelMap[label.Title]; has { *label = *l continue } -- 2.45.1 From 7bd6e910c3db3a4e663712dad0947f45a941cb9f Mon Sep 17 00:00:00 2001 From: ce72 Date: Mon, 27 Mar 2023 22:49:51 +0200 Subject: [PATCH 21/24] chore: merge upstream changes --- pkg/models/tasks.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 2e82c359b..7db8929bf 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -1513,24 +1513,21 @@ func (t *Task) overwriteRemindersWithReminderDates(reminderDates []time.Time) { func updateRelativeReminderDates(task *Task) (err error) { for _, reminder := range task.Reminders { relativeDuration := time.Duration(reminder.RelativePeriod) * time.Second + if reminder.RelativeTo != "" { + reminder.Reminder = time.Time{} + } switch reminder.RelativeTo { case ReminderRelationDueDate: if !task.DueDate.IsZero() { reminder.Reminder = task.DueDate.Add(relativeDuration) - } else { - reminder.Reminder = time.Time{} } case ReminderRelationStartDate: if !task.StartDate.IsZero() { reminder.Reminder = task.StartDate.Add(relativeDuration) - } else { - reminder.Reminder = time.Time{} } case ReminderRelationEndDate: if !task.EndDate.IsZero() { reminder.Reminder = task.EndDate.Add(relativeDuration) - } else { - reminder.Reminder = time.Time{} } default: if reminder.RelativePeriod != 0 { -- 2.45.1 From 3e49c27ad1b4d9d42db8d724b55bb37114f1c9a2 Mon Sep 17 00:00:00 2001 From: ce72 Date: Wed, 29 Mar 2023 19:24:33 +0200 Subject: [PATCH 22/24] fix: review findings --- pkg/caldav/caldav.go | 8 +-- pkg/caldav/parsing.go | 42 ++-------------- pkg/modules/migration/ticktick/ticktick.go | 35 +------------ pkg/utils/duration.go | 57 ++++++++++++++++++++++ pkg/utils/duration_test.go | 39 +++++++++++++++ 5 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 pkg/utils/duration.go create mode 100644 pkg/utils/duration_test.go diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index 830c73f94..309997538 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -62,7 +62,7 @@ type Todo struct { type Alarm struct { Time time.Time Duration time.Duration - RelativeTo string + RelativeTo models.ReminderRelation Description string } @@ -204,13 +204,13 @@ func ParseAlarms(alarms []Alarm, taskDescription string) (caldavalarms string) { caldavalarms += ` BEGIN:VALARM` switch a.RelativeTo { - case "due_date": + case models.ReminderRelationDueDate: caldavalarms += ` TRIGGER:` + makeCalDavDuration(a.Duration) - case "start_date": + case models.ReminderRelationStartDate: caldavalarms += ` TRIGGER;RELATED=START:` + makeCalDavDuration(a.Duration) - case "end_date": + case models.ReminderRelationEndDate: caldavalarms += ` TRIGGER;RELATED=END:` + makeCalDavDuration(a.Duration) default: diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 38a4d0238..004d50cbb 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -18,13 +18,13 @@ package caldav import ( "errors" - "regexp" "strconv" "strings" "time" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" + "code.vikunja.io/api/pkg/utils" ics "github.com/arran4/golang-ical" ) @@ -45,7 +45,7 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT alarms = append(alarms, Alarm{ Time: reminder.Reminder, Duration: time.Duration(reminder.RelativePeriod) * time.Second, - RelativeTo: string(reminder.RelativeTo), + RelativeTo: reminder.RelativeTo, }) } @@ -165,7 +165,7 @@ func parseVAlarm(vAlarm *ics.VAlarm, reminders []*models.TaskReminder) []*models Reminder: caldavTimeToTimestamp(property.Value)}) } case len(property.ICalParameters["RELATED"]) > 0: - duration := parseDuration(property.Value) + duration := utils.ParseISO8601Duration(property.Value) switch property.ICalParameters["RELATED"][0] { case "START": // Example: TRIGGER;RELATED=START:-P2D @@ -179,7 +179,7 @@ func parseVAlarm(vAlarm *ics.VAlarm, reminders []*models.TaskReminder) []*models RelativeTo: models.ReminderRelationEndDate}) } default: - duration := parseDuration(property.Value) + duration := utils.ParseISO8601Duration(property.Value) // Example: TRIGGER:-PT60M reminders = append(reminders, &models.TaskReminder{ RelativePeriod: int64(duration.Seconds()), @@ -213,37 +213,3 @@ func caldavTimeToTimestamp(tstring string) time.Time { } return t } - -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[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 -} - -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 -} diff --git a/pkg/modules/migration/ticktick/ticktick.go b/pkg/modules/migration/ticktick/ticktick.go index c31c8817a..861408d77 100644 --- a/pkg/modules/migration/ticktick/ticktick.go +++ b/pkg/modules/migration/ticktick/ticktick.go @@ -20,9 +20,7 @@ import ( "encoding/csv" "errors" "io" - "regexp" "sort" - "strconv" "strings" "time" @@ -30,6 +28,7 @@ import ( "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/modules/migration" "code.vikunja.io/api/pkg/user" + "code.vikunja.io/api/pkg/utils" "github.com/gocarina/gocsv" ) @@ -75,36 +74,6 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) { 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) { namespace := &models.NamespaceWithProjectsAndTasks{ Namespace: models.Namespace{ @@ -231,7 +200,7 @@ func (m *Migrator) Migrate(user *user.User, file io.ReaderAt, size int64) error task.IsChecklist = true } - reminder := parseDuration(task.ReminderDuration) + reminder := utils.ParseISO8601Duration(task.ReminderDuration) if reminder > 0 { task.Reminder = reminder } diff --git a/pkg/utils/duration.go b/pkg/utils/duration.go new file mode 100644 index 000000000..de394497f --- /dev/null +++ b/pkg/utils/duration.go @@ -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 . + +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 +} diff --git a/pkg/utils/duration_test.go b/pkg/utils/duration_test.go new file mode 100644 index 000000000..1af2e905d --- /dev/null +++ b/pkg/utils/duration_test.go @@ -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 . + +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) + }) +} -- 2.45.1 From 287a21da936d673cf8a12289958ee2ca74a638c2 Mon Sep 17 00:00:00 2001 From: ce72 Date: Wed, 29 Mar 2023 23:36:44 +0200 Subject: [PATCH 23/24] fix: match reminder paring with caldav spec and tasks.org code Follow the RFC more strictly: https://icalendar.org/iCalendar-RFC-5545/3-8-6-3-trigger.html Compare with tasks.org, to make sure their alarms will be correctly synced: https://github.com/tasks/tasks/blob/main/app/src/main/java/org/tasks/caldav/extensions/VAlarm.kt --- pkg/caldav/caldav.go | 5 +-- pkg/caldav/caldav_test.go | 2 +- pkg/caldav/parsing.go | 63 ++++++++++++++++++++------------------ pkg/caldav/parsing_test.go | 18 ++++++++--- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/pkg/caldav/caldav.go b/pkg/caldav/caldav.go index 309997538..0f53f5c1c 100644 --- a/pkg/caldav/caldav.go +++ b/pkg/caldav/caldav.go @@ -204,13 +204,10 @@ func ParseAlarms(alarms []Alarm, taskDescription string) (caldavalarms string) { caldavalarms += ` BEGIN:VALARM` switch a.RelativeTo { - case models.ReminderRelationDueDate: - caldavalarms += ` -TRIGGER:` + makeCalDavDuration(a.Duration) case models.ReminderRelationStartDate: caldavalarms += ` TRIGGER;RELATED=START:` + makeCalDavDuration(a.Duration) - case models.ReminderRelationEndDate: + case models.ReminderRelationEndDate, models.ReminderRelationDueDate: caldavalarms += ` TRIGGER;RELATED=END:` + makeCalDavDuration(a.Duration) default: diff --git a/pkg/caldav/caldav_test.go b/pkg/caldav/caldav_test.go index efaccd359..e1de0d9ca 100644 --- a/pkg/caldav/caldav_test.go +++ b/pkg/caldav/caldav_test.go @@ -311,7 +311,7 @@ ACTION:DISPLAY DESCRIPTION:alarm description END:VALARM BEGIN:VALARM -TRIGGER:-PT2H0M0S +TRIGGER;RELATED=END:-PT2H0M0S ACTION:DISPLAY DESCRIPTION:Todo #1 END:VALARM diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 004d50cbb..9b635d8c3 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -141,53 +141,56 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { vTask.EndDate = vTask.StartDate.Add(duration) } - reminders := make([]*models.TaskReminder, 0) for _, vAlarm := range vTodo.SubComponents() { if vAlarm, ok := vAlarm.(*ics.VAlarm); ok { - reminders = parseVAlarm(vAlarm, reminders) + vTask = parseVAlarm(vAlarm, vTask) } } - if len(reminders) > 0 { - vTask.Reminders = reminders - } return } -func parseVAlarm(vAlarm *ics.VAlarm, reminders []*models.TaskReminder) []*models.TaskReminder { +func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) *models.Task { for _, property := range vAlarm.UnknownPropertiesIANAProperties() { if property.IANAToken == "TRIGGER" { - switch { - case len(property.ICalParameters["VALUE"]) > 0: - if property.ICalParameters["VALUE"][0] == "DATE-TIME" { - // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z - reminders = append(reminders, &models.TaskReminder{ - Reminder: caldavTimeToTimestamp(property.Value)}) - } - case len(property.ICalParameters["RELATED"]) > 0: - duration := utils.ParseISO8601Duration(property.Value) - switch property.ICalParameters["RELATED"][0] { - case "START": + if contains(property.ICalParameters["VALUE"], "DATE-TIME") { + // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z + vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{ + Reminder: caldavTimeToTimestamp(property.Value)}) + } else { + if contains(property.ICalParameters["RELATED"], "END") { + // Example: TRIGGER;RELATED=END:-P2D + duration := utils.ParseISO8601Duration(property.Value) + 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}) + } + } else { // Example: TRIGGER;RELATED=START:-P2D - reminders = append(reminders, &models.TaskReminder{ + // Example: TRIGGER:-PT60M + duration := utils.ParseISO8601Duration(property.Value) + vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{ RelativePeriod: int64(duration.Seconds()), RelativeTo: models.ReminderRelationStartDate}) - case "END": - // Example: TRIGGER;RELATED=END:-P2D - reminders = append(reminders, &models.TaskReminder{ - RelativePeriod: int64(duration.Seconds()), - RelativeTo: models.ReminderRelationEndDate}) } - default: - duration := utils.ParseISO8601Duration(property.Value) - // Example: TRIGGER:-PT60M - reminders = append(reminders, &models.TaskReminder{ - RelativePeriod: int64(duration.Seconds()), - RelativeTo: models.ReminderRelationDueDate}) } } } - return reminders + 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 diff --git a/pkg/caldav/parsing_test.go b/pkg/caldav/parsing_test.go index 31709a7cc..b513fd0b3 100644 --- a/pkg/caldav/parsing_test.go +++ b/pkg/caldav/parsing_test.go @@ -170,7 +170,11 @@ TRIGGER:PT0S ACTION:DISPLAY END:VALARM BEGIN:VALARM -TRIGGER:-PT60M +TRIGGER;VALUE=DURATION:-PT60M +ACTION:DISPLAY +END:VALARM +BEGIN:VALARM +TRIGGER:-PT61M ACTION:DISPLAY END:VALARM BEGIN:VALARM @@ -192,19 +196,23 @@ END:VCALENDAR`, DueDate: time.Date(2023, 3, 4, 15, 0, 0, 0, config.GetTimeZone()), Reminders: []*models.TaskReminder{ { - RelativeTo: models.ReminderRelationDueDate, + RelativeTo: models.ReminderRelationStartDate, RelativePeriod: 0, }, { - RelativeTo: models.ReminderRelationDueDate, + RelativeTo: models.ReminderRelationStartDate, RelativePeriod: -3600, }, + { + RelativeTo: models.ReminderRelationStartDate, + RelativePeriod: -3660, + }, { RelativeTo: models.ReminderRelationStartDate, RelativePeriod: -86400, }, { - RelativeTo: models.ReminderRelationEndDate, + RelativeTo: models.ReminderRelationDueDate, RelativePeriod: -1800, }, }, @@ -309,7 +317,7 @@ ACTION:DISPLAY DESCRIPTION:Task 1 END:VALARM BEGIN:VALARM -TRIGGER:-PT1H0M0S +TRIGGER;RELATED=END:-PT1H0M0S ACTION:DISPLAY DESCRIPTION:Task 1 END:VALARM -- 2.45.1 From 0ef9b55536edf33d0f059de384ea2ca82fd40a1d Mon Sep 17 00:00:00 2001 From: kolaente Date: Sat, 1 Apr 2023 12:52:10 +0200 Subject: [PATCH 24/24] chore: refactor parseVAlarm --- pkg/caldav/parsing.go | 59 +++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/pkg/caldav/parsing.go b/pkg/caldav/parsing.go index 9b635d8c3..56838e455 100644 --- a/pkg/caldav/parsing.go +++ b/pkg/caldav/parsing.go @@ -152,34 +152,39 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) { func parseVAlarm(vAlarm *ics.VAlarm, vTask *models.Task) *models.Task { for _, property := range vAlarm.UnknownPropertiesIANAProperties() { - if property.IANAToken == "TRIGGER" { - if contains(property.ICalParameters["VALUE"], "DATE-TIME") { - // Example: TRIGGER;VALUE=DATE-TIME:20181201T011210Z - vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{ - Reminder: caldavTimeToTimestamp(property.Value)}) - } else { - if contains(property.ICalParameters["RELATED"], "END") { - // Example: TRIGGER;RELATED=END:-P2D - duration := utils.ParseISO8601Duration(property.Value) - 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}) - } - } else { - // Example: TRIGGER;RELATED=START:-P2D - // Example: TRIGGER:-PT60M - duration := utils.ParseISO8601Duration(property.Value) - vTask.Reminders = append(vTask.Reminders, &models.TaskReminder{ - RelativePeriod: int64(duration.Seconds()), - RelativeTo: models.ReminderRelationStartDate}) - } - } + 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 } -- 2.45.1