Add repeat mode

This commit is contained in:
kolaente 2021-04-13 14:10:33 +02:00
parent 967270b2c1
commit df4d0dd320
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
3 changed files with 236 additions and 61 deletions

View File

@ -0,0 +1,43 @@
// 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 <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type tasks20210413131057 struct {
RepeatMode int `xorm:"not null default 0" json:"repeat_mode"`
}
func (tasks20210413131057) TableName() string {
return "tasks"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20210413131057",
Description: "Add repeat mode column to tasks",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(tasks20210413131057{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -36,6 +36,13 @@ import (
"xorm.io/xorm/schemas"
)
type TaskRepeatMode int
const (
TaskRepeatModeDefault TaskRepeatMode = iota
TaskRepeatModeMonth
)
// Task represents an task in a todolist
type Task struct {
// The unique, numeric id of this task.
@ -59,6 +66,8 @@ type Task struct {
RepeatAfter int64 `xorm:"bigint 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"`
RepeatMode TaskRepeatMode `xorm:"not null default 0" json:"repeat_mode"`
// The task priority. Can be anything you want, it is possible to sort by this later.
Priority int64 `xorm:"bigint null" json:"priority"`
// When this task starts.
@ -1071,87 +1080,115 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
return updateListLastUpdated(s, &List{ID: t.ListID})
}
func addOneMonthToDate(d time.Time) time.Time {
return time.Date(d.Year(), d.Month()+1, d.Day(), d.Hour(), d.Minute(), d.Second(), d.Nanosecond(), config.GetTimeZone())
}
// This helper function updates the reminders, doneAt, start and end dates of the *old* task
// and saves the new values in the newTask object.
// We make a few assumtions here:
// 1. Everything in oldTask is the truth - we figure out if we update anything at all if oldTask.RepeatAfter has a value > 0
// 2. Because of 1., this functions should not be used to update values other than Done in the same go
func updateDone(oldTask *Task, newTask *Task) {
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
repeatDuration := time.Duration(oldTask.RepeatAfter) * time.Second
if !oldTask.Done && newTask.Done {
// 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.IsZero() {
if oldTask.RepeatFromCurrentDate {
newTask.DueDate = 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 = oldTask.DueDate.Add(repeatDuration)
// Add the repeating interval until the new due date is in the future
for !newTask.DueDate.After(now) {
newTask.DueDate = newTask.DueDate.Add(repeatDuration)
if oldTask.RepeatMode == TaskRepeatModeMonth {
if !oldTask.DueDate.IsZero() {
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.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.Reminders) > 0 {
if oldTask.RepeatFromCurrentDate {
sort.Slice(oldTask.Reminders, func(i, j int) bool {
return oldTask.Reminders[i].Unix() < oldTask.Reminders[j].Unix()
})
first := oldTask.Reminders[0]
for in, r := range oldTask.Reminders {
diff := r.Sub(first)
newTask.Reminders[in] = now.Add(repeatDuration + diff)
}
} else {
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 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.IsZero() && !oldTask.EndDate.IsZero() {
diff := oldTask.EndDate.Sub(oldTask.StartDate)
newTask.StartDate = now.Add(repeatDuration)
newTask.EndDate = now.Add(repeatDuration + diff)
} else {
if !oldTask.StartDate.IsZero() {
if oldTask.RepeatFromCurrentDate {
newTask.StartDate = now.Add(repeatDuration)
} else {
newTask.StartDate = oldTask.StartDate.Add(repeatDuration)
for !newTask.StartDate.After(now) {
newTask.StartDate = newTask.StartDate.Add(repeatDuration)
}
}
newTask.StartDate = addOneMonthToDate(oldTask.StartDate)
}
if !oldTask.EndDate.IsZero() {
if oldTask.RepeatFromCurrentDate {
newTask.EndDate = now.Add(repeatDuration)
} else {
newTask.EndDate = oldTask.EndDate.Add(repeatDuration)
for !newTask.EndDate.After(now) {
newTask.EndDate = newTask.EndDate.Add(repeatDuration)
}
}
newTask.EndDate = addOneMonthToDate(oldTask.EndDate)
}
}
newTask.Done = false
if oldTask.RepeatAfter > 0 {
repeatDuration := time.Duration(oldTask.RepeatAfter) * time.Second
// assuming we'll merge the new task over the old task
if !oldTask.DueDate.IsZero() {
if oldTask.RepeatFromCurrentDate {
newTask.DueDate = 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 = oldTask.DueDate.Add(repeatDuration)
// Add the repeating interval until the new due date is in the future
for !newTask.DueDate.After(now) {
newTask.DueDate = newTask.DueDate.Add(repeatDuration)
}
}
}
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.Reminders) > 0 {
if oldTask.RepeatFromCurrentDate {
sort.Slice(oldTask.Reminders, func(i, j int) bool {
return oldTask.Reminders[i].Unix() < oldTask.Reminders[j].Unix()
})
first := oldTask.Reminders[0]
for in, r := range oldTask.Reminders {
diff := r.Sub(first)
newTask.Reminders[in] = now.Add(repeatDuration + diff)
}
} else {
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 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.IsZero() && !oldTask.EndDate.IsZero() {
diff := oldTask.EndDate.Sub(oldTask.StartDate)
newTask.StartDate = now.Add(repeatDuration)
newTask.EndDate = now.Add(repeatDuration + diff)
} else {
if !oldTask.StartDate.IsZero() {
if oldTask.RepeatFromCurrentDate {
newTask.StartDate = now.Add(repeatDuration)
} else {
newTask.StartDate = oldTask.StartDate.Add(repeatDuration)
for !newTask.StartDate.After(now) {
newTask.StartDate = newTask.StartDate.Add(repeatDuration)
}
}
}
if !oldTask.EndDate.IsZero() {
if oldTask.RepeatFromCurrentDate {
newTask.EndDate = now.Add(repeatDuration)
} else {
newTask.EndDate = oldTask.EndDate.Add(repeatDuration)
for !newTask.EndDate.After(now) {
newTask.EndDate = newTask.EndDate.Add(repeatDuration)
}
}
}
}
newTask.Done = false
}
}
// Update the "done at" timestamp

View File

@ -541,6 +541,101 @@ func TestUpdateDone(t *testing.T) {
assert.Equal(t, time.Now().Add(diff+time.Duration(oldTask.RepeatAfter)*time.Second).Unix(), newTask.EndDate.Unix())
})
})
t.Run("repeat each month", func(t *testing.T) {
t.Run("due date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatMode: TaskRepeatModeMonth,
DueDate: time.Unix(1550000000, 0),
}
newTask := &Task{
Done: true,
}
oldDueDate := oldTask.DueDate
updateDone(oldTask, newTask)
assert.True(t, newTask.DueDate.After(oldDueDate))
assert.NotEqual(t, oldDueDate.Month(), newTask.DueDate.Month())
})
t.Run("reminders", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatMode: TaskRepeatModeMonth,
Reminders: []time.Time{
time.Unix(1550000000, 0),
time.Unix(1555000000, 0),
},
}
newTask := &Task{
Done: true,
}
oldReminders := make([]time.Time, len(oldTask.Reminders))
copy(oldReminders, oldTask.Reminders)
updateDone(oldTask, newTask)
assert.Len(t, newTask.Reminders, len(oldReminders))
for i, r := range newTask.Reminders {
assert.True(t, r.After(oldReminders[i]))
assert.NotEqual(t, oldReminders[i].Month(), r.Month())
}
})
t.Run("start date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatMode: TaskRepeatModeMonth,
StartDate: time.Unix(1550000000, 0),
}
newTask := &Task{
Done: true,
}
oldStartDate := oldTask.StartDate
updateDone(oldTask, newTask)
assert.True(t, newTask.StartDate.After(oldStartDate))
assert.NotEqual(t, oldStartDate.Month(), newTask.StartDate.Month())
})
t.Run("end date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatMode: TaskRepeatModeMonth,
EndDate: time.Unix(1560000000, 0),
}
newTask := &Task{
Done: true,
}
oldEndDate := oldTask.EndDate
updateDone(oldTask, newTask)
assert.True(t, newTask.EndDate.After(oldEndDate))
assert.NotEqual(t, oldEndDate.Month(), newTask.EndDate.Month())
})
t.Run("start and end date", func(t *testing.T) {
oldTask := &Task{
Done: false,
RepeatMode: TaskRepeatModeMonth,
StartDate: time.Unix(1550000000, 0),
EndDate: time.Unix(1560000000, 0),
}
newTask := &Task{
Done: true,
}
oldStartDate := oldTask.StartDate
oldEndDate := oldTask.EndDate
oldDiff := oldTask.EndDate.Sub(oldTask.StartDate)
updateDone(oldTask, newTask)
assert.True(t, newTask.StartDate.After(oldStartDate))
assert.NotEqual(t, oldStartDate.Month(), newTask.StartDate.Month())
assert.True(t, newTask.EndDate.After(oldEndDate))
assert.NotEqual(t, oldEndDate.Month(), newTask.EndDate.Month())
assert.Equal(t, oldDiff, newTask.EndDate.Sub(newTask.StartDate))
})
})
})
}