Add repeat mode
This commit is contained in:
parent
967270b2c1
commit
df4d0dd320
|
@ -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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -36,6 +36,13 @@ import (
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TaskRepeatMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskRepeatModeDefault TaskRepeatMode = iota
|
||||||
|
TaskRepeatModeMonth
|
||||||
|
)
|
||||||
|
|
||||||
// Task represents an task in a todolist
|
// Task represents an task in a todolist
|
||||||
type Task struct {
|
type Task struct {
|
||||||
// The unique, numeric id of this task.
|
// The unique, numeric id of this task.
|
||||||
|
@ -59,6 +66,8 @@ type Task struct {
|
||||||
RepeatAfter int64 `xorm:"bigint INDEX null" json:"repeat_after"`
|
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.
|
// 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"`
|
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.
|
// The task priority. Can be anything you want, it is possible to sort by this later.
|
||||||
Priority int64 `xorm:"bigint null" json:"priority"`
|
Priority int64 `xorm:"bigint null" json:"priority"`
|
||||||
// When this task starts.
|
// 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})
|
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
|
// This helper function updates the reminders, doneAt, start and end dates of the *old* task
|
||||||
// and saves the new values in the newTask object.
|
// and saves the new values in the newTask object.
|
||||||
// We make a few assumtions here:
|
// 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
|
// 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
|
// 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) {
|
func updateDone(oldTask *Task, newTask *Task) {
|
||||||
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
|
if !oldTask.Done && newTask.Done {
|
||||||
|
|
||||||
repeatDuration := time.Duration(oldTask.RepeatAfter) * time.Second
|
|
||||||
|
|
||||||
// Current time in an extra variable to base all calculations on the same time
|
// Current time in an extra variable to base all calculations on the same time
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// assuming we'll merge the new task over the old task
|
if oldTask.RepeatMode == TaskRepeatModeMonth {
|
||||||
if !oldTask.DueDate.IsZero() {
|
if !oldTask.DueDate.IsZero() {
|
||||||
if oldTask.RepeatFromCurrentDate {
|
newTask.DueDate = addOneMonthToDate(oldTask.DueDate)
|
||||||
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
|
newTask.Reminders = oldTask.Reminders
|
||||||
// but not the repeating interval
|
if len(oldTask.Reminders) > 0 {
|
||||||
newTask.DueDate = oldTask.DueDate.Add(repeatDuration)
|
for in, r := range oldTask.Reminders {
|
||||||
// Add the repeating interval until the new due date is in the future
|
newTask.Reminders[in] = addOneMonthToDate(r)
|
||||||
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.StartDate.IsZero() {
|
||||||
if oldTask.RepeatFromCurrentDate {
|
newTask.StartDate = addOneMonthToDate(oldTask.StartDate)
|
||||||
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.EndDate.IsZero() {
|
||||||
if oldTask.RepeatFromCurrentDate {
|
newTask.EndDate = addOneMonthToDate(oldTask.EndDate)
|
||||||
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
|
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
|
// Update the "done at" timestamp
|
||||||
|
|
|
@ -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())
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue