1
0

Repeat tasks after completion (#587)

Fix integration tests

Add updating start and end date from current date

Add updating reminders from current date

Add updating reminders from current date

Add updating due date from current date

Add updating flag

Add tests

Add field to repeat a task after current date

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/587
This commit is contained in:
konrad
2020-06-14 11:04:38 +00:00
parent 3ba26ce67e
commit 47d7e713af
4 changed files with 213 additions and 42 deletions

View File

@ -26,6 +26,7 @@ import (
"code.vikunja.io/web"
"github.com/imdario/mergo"
"math"
"sort"
"strconv"
"time"
"xorm.io/builder"
@ -53,6 +54,8 @@ type Task struct {
ListID int64 `xorm:"int(11) 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.
RepeatAfter int64 `xorm:"int(11) INDEX null" json:"repeat_after"`
// If specified, a repeating task will repeat from the current date rather than the last set date.
RepeatFromCurrentDate bool `xorm:"null" json:"repeat_from_current_date"`
// The task priority. Can be anything you want, it is possible to sort by this later.
Priority int64 `xorm:"int(11) null" json:"priority"`
// When this task starts.
@ -761,6 +764,10 @@ func (t *Task) Update() (err error) {
if t.Position == 0 {
ot.Position = 0
}
// Repeat from current date
if !t.RepeatFromCurrentDate {
ot.RepeatFromCurrentDate = false
}
_, err = x.ID(t.ID).
Cols("title",
@ -777,6 +784,7 @@ func (t *Task) Update() (err error) {
"list_id",
"bucket_id",
"position",
"repeat_from_current_date",
).
Update(ot)
*t = ot
@ -798,36 +806,73 @@ func updateDone(oldTask *Task, newTask *Task) {
repeatDuration := time.Duration(oldTask.RepeatAfter) * time.Second
// Current time in an extra variable to base all calculations on the same time
now := time.Now()
// assuming we'll merge the new task over the old task
if oldTask.DueDate > 0 {
// Always add one instance of the repeating interval to catch cases where a due date is already in the future
// but not the repeating interval
newTask.DueDate = timeutil.FromTime(oldTask.DueDate.ToTime().Add(repeatDuration))
// Add the repeating interval until the new due date is in the future
for !newTask.DueDate.ToTime().After(time.Now()) {
newTask.DueDate = timeutil.FromTime(newTask.DueDate.ToTime().Add(repeatDuration))
if oldTask.RepeatFromCurrentDate {
newTask.DueDate = timeutil.FromTime(now.Add(repeatDuration))
} else {
// Always add one instance of the repeating interval to catch cases where a due date is already in the future
// but not the repeating interval
newTask.DueDate = timeutil.FromTime(oldTask.DueDate.ToTime().Add(repeatDuration))
// Add the repeating interval until the new due date is in the future
for !newTask.DueDate.ToTime().After(now) {
newTask.DueDate = timeutil.FromTime(newTask.DueDate.ToTime().Add(repeatDuration))
}
}
}
newTask.Reminders = oldTask.Reminders
for in, r := range oldTask.Reminders {
newTask.Reminders[in] = timeutil.FromTime(r.ToTime().Add(repeatDuration))
for !newTask.Reminders[in].ToTime().After(time.Now()) {
newTask.Reminders[in] = timeutil.FromTime(newTask.Reminders[in].ToTime().Add(repeatDuration))
// 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 {
if oldTask.RepeatFromCurrentDate {
sort.Slice(oldTask.Reminders, func(i, j int) bool {
return oldTask.Reminders[i] < oldTask.Reminders[j]
})
first := oldTask.Reminders[0]
for in, r := range oldTask.Reminders {
diff := time.Duration(r-first) * time.Second
newTask.Reminders[in] = timeutil.FromTime(now.Add(repeatDuration + diff))
}
} else {
for in, r := range oldTask.Reminders {
newTask.Reminders[in] = timeutil.FromTime(r.ToTime().Add(repeatDuration))
for !newTask.Reminders[in].ToTime().After(now) {
newTask.Reminders[in] = timeutil.FromTime(newTask.Reminders[in].ToTime().Add(repeatDuration))
}
}
}
}
if oldTask.StartDate > 0 {
newTask.StartDate = timeutil.FromTime(oldTask.StartDate.ToTime().Add(repeatDuration))
for !newTask.StartDate.ToTime().After(time.Now()) {
newTask.StartDate = timeutil.FromTime(newTask.StartDate.ToTime().Add(repeatDuration))
// If a task has a start and end date, the end date should keep the difference to the start date when setting them as new
if oldTask.RepeatFromCurrentDate && oldTask.StartDate > 0 && oldTask.EndDate > 0 {
diff := time.Duration(oldTask.EndDate-oldTask.StartDate) * time.Second
newTask.StartDate = timeutil.FromTime(now.Add(repeatDuration))
newTask.EndDate = timeutil.FromTime(now.Add(repeatDuration + diff))
} else {
if oldTask.StartDate > 0 {
if oldTask.RepeatFromCurrentDate {
newTask.StartDate = timeutil.FromTime(now.Add(repeatDuration))
} else {
newTask.StartDate = timeutil.FromTime(oldTask.StartDate.ToTime().Add(repeatDuration))
for !newTask.StartDate.ToTime().After(now) {
newTask.StartDate = timeutil.FromTime(newTask.StartDate.ToTime().Add(repeatDuration))
}
}
}
}
if oldTask.EndDate > 0 {
newTask.EndDate = timeutil.FromTime(oldTask.EndDate.ToTime().Add(repeatDuration))
for !newTask.EndDate.ToTime().After(time.Now()) {
newTask.EndDate = timeutil.FromTime(newTask.EndDate.ToTime().Add(repeatDuration))
if oldTask.EndDate > 0 {
if oldTask.RepeatFromCurrentDate {
newTask.EndDate = timeutil.FromTime(now.Add(repeatDuration))
} else {
newTask.EndDate = timeutil.FromTime(oldTask.EndDate.ToTime().Add(repeatDuration))
for !newTask.EndDate.ToTime().After(now) {
newTask.EndDate = timeutil.FromTime(newTask.EndDate.ToTime().Add(repeatDuration))
}
}
}
}

View File

@ -248,6 +248,89 @@ func TestUpdateDone(t *testing.T) {
expected := int64(oldTask.DueDate) + oldTask.RepeatAfter
assert.Equal(t, timeutil.TimeStamp(expected), newTask.DueDate)
})
t.Run("repeat from current date", func(t *testing.T) {
t.Run("due date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatAfter: 8600,
RepeatFromCurrentDate: true,
DueDate: timeutil.TimeStamp(1550000000),
}
newTask := &Task{
Done: true,
}
updateDone(oldTask, newTask)
assert.Equal(t, timeutil.FromTime(time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second)), newTask.DueDate)
})
t.Run("reminders", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatAfter: 8600,
RepeatFromCurrentDate: true,
Reminders: []timeutil.TimeStamp{
1550000000,
1555000000,
},
}
newTask := &Task{
Done: true,
}
updateDone(oldTask, newTask)
diff := time.Duration(oldTask.Reminders[1]-oldTask.Reminders[0]) * time.Second
assert.Len(t, newTask.Reminders, 2)
assert.Equal(t, timeutil.FromTime(time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second)), newTask.Reminders[0])
assert.Equal(t, timeutil.FromTime(time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second)), newTask.Reminders[1])
})
t.Run("start date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatAfter: 8600,
RepeatFromCurrentDate: true,
StartDate: timeutil.TimeStamp(1550000000),
}
newTask := &Task{
Done: true,
}
updateDone(oldTask, newTask)
assert.Equal(t, timeutil.FromTime(time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second)), newTask.StartDate)
})
t.Run("end date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatAfter: 8600,
RepeatFromCurrentDate: true,
EndDate: timeutil.TimeStamp(1560000000),
}
newTask := &Task{
Done: true,
}
updateDone(oldTask, newTask)
assert.Equal(t, timeutil.FromTime(time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second)), newTask.EndDate)
})
t.Run("start and end date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatAfter: 8600,
RepeatFromCurrentDate: true,
StartDate: timeutil.TimeStamp(1550000000),
EndDate: timeutil.TimeStamp(1560000000),
}
newTask := &Task{
Done: true,
}
updateDone(oldTask, newTask)
diff := time.Duration(oldTask.EndDate-oldTask.StartDate) * time.Second
assert.Equal(t, timeutil.FromTime(time.Now().Add(time.Duration(oldTask.RepeatAfter)*time.Second)), newTask.StartDate)
assert.Equal(t, timeutil.FromTime(time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second)), newTask.EndDate)
})
})
})
}