feat: add setting to change overdue tasks reminder email time

This commit is contained in:
kolaente 2022-06-16 16:20:26 +02:00
parent 030bbfa47e
commit 8869adfc27
No known key found for this signature in database
GPG key ID: F40E70337AB24C9B
17 changed files with 129 additions and 33 deletions

View file

@ -43,6 +43,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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,
}

View file

@ -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]

View file

@ -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).

View file

@ -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,
}

View file

@ -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 {

View file

@ -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,

View file

@ -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
View 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
}

View file

@ -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"

View file

@ -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"

View file

@ -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.

View file

@ -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 {

View file

@ -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)
}