Add repeat monthly setting for tasks #834
43
pkg/migration/20210413131057.go
Normal file
43
pkg/migration/20210413131057.go
Normal 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
|
||||
},
|
||||
})
|
||||
}
|
@ -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
|
||||
|
@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user