feat: add setting to change overdue tasks reminder email time
This commit is contained in:
parent
030bbfa47e
commit
8869adfc27
17 changed files with 129 additions and 33 deletions
|
@ -43,6 +43,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -54,6 +54,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -104,6 +105,7 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -168,6 +170,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -229,6 +232,7 @@ func TestLabel_ReadOne(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -151,6 +151,7 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -164,6 +165,7 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -150,6 +150,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
@ -163,6 +164,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
|
|
|
@ -37,6 +37,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -47,6 +48,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -57,6 +59,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ import (
|
|||
)
|
||||
|
||||
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[int64]*userWithTasks, err error) {
|
||||
now = utils.GetTimeWithoutNanoSeconds(now)
|
||||
now = utils.GetTimeWithoutSeconds(now)
|
||||
nextMinute := now.Add(1 * time.Minute)
|
||||
|
||||
var tasks []*Task
|
||||
|
@ -78,10 +78,14 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
|
|||
tzs[t.User.Timezone] = tz
|
||||
}
|
||||
|
||||
// If it is 9:00 for that current user, add the task to their list of overdue tasks
|
||||
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, tz)
|
||||
// If it is time for that current user, add the task to their list of overdue tasks
|
||||
tm, err := time.Parse("15:04", t.User.OverdueTasksRemindersTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), tm.Hour(), tm.Minute(), 0, 0, tz)
|
||||
isTimeForReminder := overdueMailTime.After(now) || overdueMailTime.Equal(now.In(tz))
|
||||
wasTimeForReminder := overdueMailTime.Before(now.Add(time.Minute))
|
||||
wasTimeForReminder := overdueMailTime.Before(nextMinute)
|
||||
taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz))
|
||||
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
|
||||
_, exists := uts[t.User.ID]
|
||||
|
|
|
@ -61,7 +61,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
|
|||
// Get all creators of tasks
|
||||
creators := make(map[int64]*user.User, len(taskIDs))
|
||||
err = s.
|
||||
Select("users.id, users.username, users.email, users.name, users.timezone").
|
||||
Select("users.*").
|
||||
Join("LEFT", "tasks", "tasks.created_by_id = users.id").
|
||||
In("tasks.id", taskIDs).
|
||||
Where(cond).
|
||||
|
|
|
@ -32,6 +32,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -42,6 +43,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -52,6 +54,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -63,6 +66,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -74,6 +78,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -84,6 +89,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -95,6 +101,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
EmailRemindersEnabled: true,
|
||||
DiscoverableByEmail: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -105,6 +112,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -115,6 +123,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -125,6 +134,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -136,6 +146,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -148,6 +159,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
EmailRemindersEnabled: true,
|
||||
DiscoverableByName: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
@ -158,6 +170,7 @@ func TestListUsersFromList(t *testing.T) {
|
|||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
OverdueTasksRemindersEnabled: true,
|
||||
OverdueTasksRemindersTime: "09:00",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -46,11 +48,13 @@ type UserSettings struct {
|
|||
DiscoverableByEmail bool `json:"discoverable_by_email"`
|
||||
// If enabled, the user will get an email for their overdue tasks each morning.
|
||||
OverdueTasksRemindersEnabled bool `json:"overdue_tasks_reminders_enabled"`
|
||||
// The time when the daily summary of overdue tasks will be sent via email.
|
||||
OverdueTasksRemindersTime string `json:"overdue_tasks_reminders_time" valid:"time,required"`
|
||||
// If a task is created without a specified list this value should be used. Applies
|
||||
// to tasks made directly in API and from clients.
|
||||
DefaultListID int64 `json:"default_list_id"`
|
||||
// The day when the week starts for this user. 0 = sunday, 1 = monday, etc.
|
||||
WeekStart int `json:"week_start"`
|
||||
WeekStart int `json:"week_start" valid:"range(0|7)"`
|
||||
// The user's language
|
||||
Language string `json:"language"`
|
||||
// The user's time zone. Used to send task reminders in the time zone of the user.
|
||||
|
@ -158,7 +162,16 @@ func UpdateGeneralUserSettings(c echo.Context) error {
|
|||
us := &UserSettings{}
|
||||
err := c.Bind(us)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad user name provided.")
|
||||
var he *echo.HTTPError
|
||||
if errors.As(err, &he) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
|
||||
}
|
||||
|
||||
err = c.Validate(us)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err)
|
||||
}
|
||||
|
||||
u, err := user2.GetCurrentUser(c)
|
||||
|
@ -184,6 +197,7 @@ func UpdateGeneralUserSettings(c echo.Context) error {
|
|||
user.WeekStart = us.WeekStart
|
||||
user.Language = us.Language
|
||||
user.Timezone = us.Timezone
|
||||
user.OverdueTasksRemindersTime = us.OverdueTasksRemindersTime
|
||||
|
||||
_, err = user2.UpdateUser(s, user)
|
||||
if err != nil {
|
||||
|
|
|
@ -75,6 +75,7 @@ func UserShow(c echo.Context) error {
|
|||
WeekStart: u.WeekStart,
|
||||
Language: u.Language,
|
||||
Timezone: u.Timezone,
|
||||
OverdueTasksRemindersTime: u.OverdueTasksRemindersTime,
|
||||
},
|
||||
DeletionScheduledAt: u.DeletionScheduledAt,
|
||||
IsLocalUser: u.Issuer == user.IssuerLocal,
|
||||
|
|
|
@ -79,7 +79,6 @@ import (
|
|||
"code.vikunja.io/web"
|
||||
"code.vikunja.io/web/handler"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/getsentry/sentry-go"
|
||||
sentryecho "github.com/getsentry/sentry-go/echo"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
@ -88,31 +87,6 @@ import (
|
|||
elog "github.com/labstack/gommon/log"
|
||||
)
|
||||
|
||||
// CustomValidator is a dummy struct to use govalidator with echo
|
||||
type CustomValidator struct{}
|
||||
|
||||
// Validate validates stuff
|
||||
func (cv *CustomValidator) Validate(i interface{}) error {
|
||||
if _, err := govalidator.ValidateStruct(i); err != nil {
|
||||
|
||||
var errs []string
|
||||
for field, e := range govalidator.ErrorsByField(err) {
|
||||
errs = append(errs, field+": "+e)
|
||||
}
|
||||
|
||||
httperr := models.ValidationHTTPError{
|
||||
HTTPError: web.HTTPError{
|
||||
Code: models.ErrCodeInvalidData,
|
||||
Message: "Invalid Data",
|
||||
},
|
||||
InvalidFields: errs,
|
||||
}
|
||||
|
||||
return httperr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewEcho registers a new Echo instance
|
||||
func NewEcho() *echo.Echo {
|
||||
e := echo.New()
|
||||
|
|
55
pkg/routes/validation.go
Normal file
55
pkg/routes/validation.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
// 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 routes
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
|
||||
"code.vikunja.io/web"
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
// CustomValidator is a dummy struct to use govalidator with echo
|
||||
type CustomValidator struct{}
|
||||
|
||||
func init() {
|
||||
govalidator.TagMap["time"] = govalidator.Validator(func(str string) bool {
|
||||
return govalidator.IsTime(str, "15:04")
|
||||
})
|
||||
}
|
||||
|
||||
// Validate validates stuff
|
||||
func (cv *CustomValidator) Validate(i interface{}) error {
|
||||
if _, err := govalidator.ValidateStruct(i); err != nil {
|
||||
|
||||
var errs []string
|
||||
for field, e := range govalidator.ErrorsByField(err) {
|
||||
errs = append(errs, field+": "+e)
|
||||
}
|
||||
|
||||
httperr := models.ValidationHTTPError{
|
||||
HTTPError: web.HTTPError{
|
||||
Code: models.ErrCodeInvalidData,
|
||||
Message: "Invalid Data",
|
||||
},
|
||||
InvalidFields: errs,
|
||||
}
|
||||
|
||||
return httperr
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -9184,6 +9184,10 @@ const docTemplate = `{
|
|||
"description": "If enabled, the user will get an email for their overdue tasks each morning.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"overdue_tasks_reminders_time": {
|
||||
"description": "The time when the daily summary of overdue tasks will be sent via email.",
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"description": "The user's time zone. Used to send task reminders in the time zone of the user.",
|
||||
"type": "string"
|
||||
|
|
|
@ -9175,6 +9175,10 @@
|
|||
"description": "If enabled, the user will get an email for their overdue tasks each morning.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"overdue_tasks_reminders_time": {
|
||||
"description": "The time when the daily summary of overdue tasks will be sent via email.",
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"description": "The user's time zone. Used to send task reminders in the time zone of the user.",
|
||||
"type": "string"
|
||||
|
|
|
@ -1310,6 +1310,10 @@ definitions:
|
|||
description: If enabled, the user will get an email for their overdue tasks
|
||||
each morning.
|
||||
type: boolean
|
||||
overdue_tasks_reminders_time:
|
||||
description: The time when the daily summary of overdue tasks will be sent
|
||||
via email.
|
||||
type: string
|
||||
timezone:
|
||||
description: The user's time zone. Used to send task reminders in the time
|
||||
zone of the user.
|
||||
|
|
|
@ -94,6 +94,7 @@ type User struct {
|
|||
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
|
||||
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
|
||||
OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"`
|
||||
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
|
||||
DefaultListID int64 `xorm:"bigint null index" json:"-"`
|
||||
WeekStart int `xorm:"null" json:"-"`
|
||||
Language string `xorm:"varchar(50) null" json:"-"`
|
||||
|
@ -493,6 +494,7 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) {
|
|||
"week_start",
|
||||
"language",
|
||||
"timezone",
|
||||
"overdue_tasks_reminders_time",
|
||||
).
|
||||
Update(user)
|
||||
if err != nil {
|
||||
|
|
|
@ -30,3 +30,12 @@ func GetTimeWithoutNanoSeconds(t time.Time) time.Time {
|
|||
// so we make sure the time we use to get the reminders don't contain nanoseconds.
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()).In(tz)
|
||||
}
|
||||
|
||||
// GetTimeWithoutSeconds returns a time.Time with the seconds set to 0.
|
||||
func GetTimeWithoutSeconds(t time.Time) time.Time {
|
||||
tz := config.GetTimeZone()
|
||||
|
||||
// By default, time.Now() includes nanoseconds which we don't save. That results in getting the wrong dates,
|
||||
// so we make sure the time we use to get the reminders don't contain nanoseconds.
|
||||
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location()).In(tz)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue