Improve sending overdue task reminders by only sending one for all overdue tasks
This commit is contained in:
parent
7ff7b0d743
commit
6e263b6a91
6 changed files with 118 additions and 28 deletions
3
go.mod
3
go.mod
|
@ -69,6 +69,7 @@ require (
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
github.com/swaggo/swag v1.7.0
|
github.com/swaggo/swag v1.7.0
|
||||||
github.com/ulule/limiter/v3 v3.8.0
|
github.com/ulule/limiter/v3 v3.8.0
|
||||||
|
github.com/yuin/goldmark v1.3.5
|
||||||
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc
|
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc
|
||||||
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
|
||||||
golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78
|
golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78
|
||||||
|
@ -99,4 +100,4 @@ replace (
|
||||||
gopkg.in/fsnotify.v1 => github.com/kolaente/fsnotify v1.4.10-0.20200411160148-1bc3c8ff4048 // See https://github.com/fsnotify/fsnotify/issues/328 and https://github.com/golang/go/issues/26904
|
gopkg.in/fsnotify.v1 => github.com/kolaente/fsnotify v1.4.10-0.20200411160148-1bc3c8ff4048 // See https://github.com/fsnotify/fsnotify/issues/328 and https://github.com/golang/go/issues/26904
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.13
|
go 1.15
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -743,6 +743,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
|
|
@ -214,3 +214,37 @@ func (n *UndoneTaskOverdueNotification) ToDB() interface{} {
|
||||||
func (n *UndoneTaskOverdueNotification) Name() string {
|
func (n *UndoneTaskOverdueNotification) Name() string {
|
||||||
return "task.undone.overdue"
|
return "task.undone.overdue"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
|
||||||
|
type UndoneTasksOverdueNotification struct {
|
||||||
|
User *user.User
|
||||||
|
Tasks []*Task
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMail returns the mail notification for UndoneTasksOverdueNotification
|
||||||
|
func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
|
||||||
|
|
||||||
|
overdueLine := ""
|
||||||
|
for _, task := range n.Tasks {
|
||||||
|
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
|
||||||
|
overdueLine += `* [` + task.Title + `](` + config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `), overdue since ` + utils.HumanizeDuration(until) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications.NewMail().
|
||||||
|
Subject(`Your overdue tasks`).
|
||||||
|
Greeting("Hi "+n.User.GetName()+",").
|
||||||
|
Line("You have the following overdue tasks:").
|
||||||
|
Line(overdueLine).
|
||||||
|
Action("Open Vikunja", config.ServiceFrontendurl.GetString()).
|
||||||
|
Line("Have a nice day!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDB returns the UndoneTasksOverdueNotification notification in a format which can be saved in the db
|
||||||
|
func (n *UndoneTasksOverdueNotification) ToDB() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the notification
|
||||||
|
func (n *UndoneTasksOverdueNotification) Name() string {
|
||||||
|
return "task.undone.overdue"
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ package models
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.vikunja.io/api/pkg/user"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/config"
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/cron"
|
"code.vikunja.io/api/pkg/cron"
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
|
@ -48,6 +50,11 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type userWithTasks struct {
|
||||||
|
user *user.User
|
||||||
|
tasks []*Task
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
|
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
|
||||||
func RegisterOverdueReminderCron() {
|
func RegisterOverdueReminderCron() {
|
||||||
if !config.ServiceEnableEmailReminders.GetBool() {
|
if !config.ServiceEnableEmailReminders.GetBool() {
|
||||||
|
@ -76,21 +83,41 @@ func RegisterOverdueReminderCron() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users))
|
uts := make(map[int64]*userWithTasks)
|
||||||
|
for _, t := range users {
|
||||||
for _, u := range users {
|
_, exists := uts[t.User.ID]
|
||||||
n := &UndoneTaskOverdueNotification{
|
if !exists {
|
||||||
User: u.User,
|
uts[t.User.ID] = &userWithTasks{
|
||||||
Task: u.Task,
|
user: t.User,
|
||||||
|
tasks: []*Task{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = notifications.Notify(u.User, n)
|
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users))
|
||||||
|
|
||||||
|
for _, ut := range uts {
|
||||||
|
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
||||||
|
User: ut.user,
|
||||||
|
Tasks: ut.tasks,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ut.tasks) == 1 {
|
||||||
|
n = &UndoneTaskOverdueNotification{
|
||||||
|
User: ut.user,
|
||||||
|
Task: ut.tasks[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = notifications.Notify(ut.user, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not notify user %d: %s", u.User.ID, err)
|
log.Errorf("[Undone Overdue Tasks Reminder] Could not notify user %d: %s", ut.user.ID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID)
|
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -24,6 +24,8 @@ import (
|
||||||
"code.vikunja.io/api/pkg/config"
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/mail"
|
"code.vikunja.io/api/pkg/mail"
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark"
|
||||||
)
|
)
|
||||||
|
|
||||||
const mailTemplatePlain = `
|
const mailTemplatePlain = `
|
||||||
|
@ -54,10 +56,8 @@ const mailTemplateHTML = `
|
||||||
{{ .Greeting }}
|
{{ .Greeting }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{{ range $line := .IntroLines}}
|
{{ range $line := .IntroLinesHTML}}
|
||||||
<p>
|
|
||||||
{{ $line }}
|
{{ $line }}
|
||||||
</p>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .ActionURL }}
|
{{ if .ActionURL }}
|
||||||
|
@ -67,10 +67,8 @@ const mailTemplateHTML = `
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{ range $line := .OutroLines}}
|
{{ range $line := .OutroLinesHTML}}
|
||||||
<p>
|
|
||||||
{{ $line }}
|
{{ $line }}
|
||||||
</p>
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .ActionURL }}
|
{{ if .ActionURL }}
|
||||||
|
@ -114,6 +112,32 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) {
|
||||||
data["Boundary"] = boundary
|
data["Boundary"] = boundary
|
||||||
data["FrontendURL"] = config.ServiceFrontendurl.GetString()
|
data["FrontendURL"] = config.ServiceFrontendurl.GetString()
|
||||||
|
|
||||||
|
var introLinesHTML []templatehtml.HTML
|
||||||
|
for _, line := range m.introLines {
|
||||||
|
md := []byte(templatehtml.HTMLEscapeString(line))
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = goldmark.Convert(md, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//#nosec - the html is escaped few lines before
|
||||||
|
introLinesHTML = append(introLinesHTML, templatehtml.HTML(buf.String()))
|
||||||
|
}
|
||||||
|
data["IntroLinesHTML"] = introLinesHTML
|
||||||
|
|
||||||
|
var outroLinesHTML []templatehtml.HTML
|
||||||
|
for _, line := range m.outroLines {
|
||||||
|
md := []byte(templatehtml.HTMLEscapeString(line))
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = goldmark.Convert(md, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
//#nosec - the html is escaped few lines before
|
||||||
|
outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(buf.String()))
|
||||||
|
}
|
||||||
|
data["OutroLinesHTML"] = outroLinesHTML
|
||||||
|
|
||||||
err = plain.Execute(&plainContent, data)
|
err = plain.Execute(&plainContent, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -90,6 +90,7 @@ func TestRenderMail(t *testing.T) {
|
||||||
Subject("Testmail").
|
Subject("Testmail").
|
||||||
Greeting("Hi there,").
|
Greeting("Hi there,").
|
||||||
Line("This is a line").
|
Line("This is a line").
|
||||||
|
Line("This **line** contains [a link](https://vikunja.io)").
|
||||||
Line("And another one").
|
Line("And another one").
|
||||||
Action("The action", "https://example.com").
|
Action("The action", "https://example.com").
|
||||||
Line("This should be an outro line").
|
Line("This should be an outro line").
|
||||||
|
@ -105,6 +106,8 @@ Hi there,
|
||||||
|
|
||||||
This is a line
|
This is a line
|
||||||
|
|
||||||
|
This **line** contains [a link](https://vikunja.io)
|
||||||
|
|
||||||
And another one
|
And another one
|
||||||
|
|
||||||
The action:
|
The action:
|
||||||
|
@ -132,13 +135,14 @@ And one more, because why not?
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<p>
|
<p>This is a line</p>
|
||||||
This is a line
|
|
||||||
</p>
|
|
||||||
|
<p>This <strong>line</strong> contains <a href="https://vikunja.io">a link</a></p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>And another one</p>
|
||||||
|
|
||||||
<p>
|
|
||||||
And another one
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,13 +153,11 @@ And one more, because why not?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<p>
|
<p>This should be an outro line</p>
|
||||||
This should be an outro line
|
|
||||||
</p>
|
|
||||||
|
<p>And one more, because why not?</p>
|
||||||
|
|
||||||
<p>
|
|
||||||
And one more, because why not?
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue