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", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View file

@ -54,6 +54,7 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -104,6 +105,7 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },
@ -168,6 +170,7 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -229,6 +232,7 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View file

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

View file

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

View file

@ -37,6 +37,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -47,6 +48,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -57,6 +59,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }

View file

@ -32,7 +32,7 @@ import (
) )
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[int64]*userWithTasks, err error) { 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) nextMinute := now.Add(1 * time.Minute)
var tasks []*Task var tasks []*Task
@ -78,10 +78,14 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
tzs[t.User.Timezone] = tz tzs[t.User.Timezone] = tz
} }
// If it is 9:00 for that current user, add the task to their list of overdue tasks // If it is time 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) 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)) 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)) taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz))
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone { if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
_, exists := uts[t.User.ID] _, 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 // Get all creators of tasks
creators := make(map[int64]*user.User, len(taskIDs)) creators := make(map[int64]*user.User, len(taskIDs))
err = s. err = s.
Select("users.id, users.username, users.email, users.name, users.timezone"). Select("users.*").
Join("LEFT", "tasks", "tasks.created_by_id = users.id"). Join("LEFT", "tasks", "tasks.created_by_id = users.id").
In("tasks.id", taskIDs). In("tasks.id", taskIDs).
Where(cond). Where(cond).

View file

@ -32,6 +32,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -42,6 +43,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -52,6 +54,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -63,6 +66,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -74,6 +78,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -84,6 +89,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -95,6 +101,7 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
DiscoverableByEmail: true, DiscoverableByEmail: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -105,6 +112,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -115,6 +123,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -125,6 +134,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -136,6 +146,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -148,6 +159,7 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
DiscoverableByName: true, DiscoverableByName: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -158,6 +170,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }

View file

@ -17,6 +17,8 @@
package v1 package v1
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -46,11 +48,13 @@ type UserSettings struct {
DiscoverableByEmail bool `json:"discoverable_by_email"` DiscoverableByEmail bool `json:"discoverable_by_email"`
// If enabled, the user will get an email for their overdue tasks each morning. // If enabled, the user will get an email for their overdue tasks each morning.
OverdueTasksRemindersEnabled bool `json:"overdue_tasks_reminders_enabled"` 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 // If a task is created without a specified list this value should be used. Applies
// to tasks made directly in API and from clients. // to tasks made directly in API and from clients.
DefaultListID int64 `json:"default_list_id"` DefaultListID int64 `json:"default_list_id"`
// The day when the week starts for this user. 0 = sunday, 1 = monday, etc. // 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 // The user's language
Language string `json:"language"` Language string `json:"language"`
// The user's time zone. Used to send task reminders in the time zone of the user. // 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{} us := &UserSettings{}
err := c.Bind(us) err := c.Bind(us)
if err != nil { 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) u, err := user2.GetCurrentUser(c)
@ -184,6 +197,7 @@ func UpdateGeneralUserSettings(c echo.Context) error {
user.WeekStart = us.WeekStart user.WeekStart = us.WeekStart
user.Language = us.Language user.Language = us.Language
user.Timezone = us.Timezone user.Timezone = us.Timezone
user.OverdueTasksRemindersTime = us.OverdueTasksRemindersTime
_, err = user2.UpdateUser(s, user) _, err = user2.UpdateUser(s, user)
if err != nil { if err != nil {

View file

@ -75,6 +75,7 @@ func UserShow(c echo.Context) error {
WeekStart: u.WeekStart, WeekStart: u.WeekStart,
Language: u.Language, Language: u.Language,
Timezone: u.Timezone, Timezone: u.Timezone,
OverdueTasksRemindersTime: u.OverdueTasksRemindersTime,
}, },
DeletionScheduledAt: u.DeletionScheduledAt, DeletionScheduledAt: u.DeletionScheduledAt,
IsLocalUser: u.Issuer == user.IssuerLocal, IsLocalUser: u.Issuer == user.IssuerLocal,

View file

@ -79,7 +79,6 @@ import (
"code.vikunja.io/web" "code.vikunja.io/web"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/asaskevich/govalidator"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo" sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
@ -88,31 +87,6 @@ import (
elog "github.com/labstack/gommon/log" 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 // NewEcho registers a new Echo instance
func NewEcho() *echo.Echo { func NewEcho() *echo.Echo {
e := echo.New() 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.", "description": "If enabled, the user will get an email for their overdue tasks each morning.",
"type": "boolean" "type": "boolean"
}, },
"overdue_tasks_reminders_time": {
"description": "The time when the daily summary of overdue tasks will be sent via email.",
"type": "string"
},
"timezone": { "timezone": {
"description": "The user's time zone. Used to send task reminders in the time zone of the user.", "description": "The user's time zone. Used to send task reminders in the time zone of the user.",
"type": "string" "type": "string"

View file

@ -9175,6 +9175,10 @@
"description": "If enabled, the user will get an email for their overdue tasks each morning.", "description": "If enabled, the user will get an email for their overdue tasks each morning.",
"type": "boolean" "type": "boolean"
}, },
"overdue_tasks_reminders_time": {
"description": "The time when the daily summary of overdue tasks will be sent via email.",
"type": "string"
},
"timezone": { "timezone": {
"description": "The user's time zone. Used to send task reminders in the time zone of the user.", "description": "The user's time zone. Used to send task reminders in the time zone of the user.",
"type": "string" "type": "string"

View file

@ -1310,6 +1310,10 @@ definitions:
description: If enabled, the user will get an email for their overdue tasks description: If enabled, the user will get an email for their overdue tasks
each morning. each morning.
type: boolean type: boolean
overdue_tasks_reminders_time:
description: The time when the daily summary of overdue tasks will be sent
via email.
type: string
timezone: timezone:
description: The user's time zone. Used to send task reminders in the time description: The user's time zone. Used to send task reminders in the time
zone of the user. zone of the user.

View file

@ -94,6 +94,7 @@ type User struct {
DiscoverableByName bool `xorm:"bool default false index" json:"-"` DiscoverableByName bool `xorm:"bool default false index" json:"-"`
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"` DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
OverdueTasksRemindersEnabled bool `xorm:"bool default true 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:"-"` DefaultListID int64 `xorm:"bigint null index" json:"-"`
WeekStart int `xorm:"null" json:"-"` WeekStart int `xorm:"null" json:"-"`
Language string `xorm:"varchar(50) 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", "week_start",
"language", "language",
"timezone", "timezone",
"overdue_tasks_reminders_time",
). ).
Update(user) Update(user)
if err != nil { 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. // 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) 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)
}