mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-05-10 15:15:41 -05:00
The call to i18n.T for notifications.task.overdue.overdue was missing its first positional argument, so the translation key was being passed as the language code. This surfaced as a "dead key" once the translation check learned to look for unused entries. Fix the call so the reminder line is properly localised.
471 lines
18 KiB
Go
471 lines
18 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present 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 License 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 License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/config"
|
|
"code.vikunja.io/api/pkg/db"
|
|
"code.vikunja.io/api/pkg/i18n"
|
|
"code.vikunja.io/api/pkg/mail"
|
|
"code.vikunja.io/api/pkg/modules/avatar"
|
|
"code.vikunja.io/api/pkg/notifications"
|
|
"code.vikunja.io/api/pkg/user"
|
|
"code.vikunja.io/api/pkg/utils"
|
|
)
|
|
|
|
// getDoerAvatarDataURI returns the avatar data URI for a user, for use in email headers.
|
|
func getDoerAvatarDataURI(doer *user.User) string {
|
|
provider := avatar.GetProvider(doer)
|
|
dataURI, err := provider.AsDataURI(doer, 20)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return dataURI
|
|
}
|
|
|
|
// getThreadID generates a Message-ID format thread ID for a task
|
|
func getThreadID(taskID int64) string {
|
|
return fmt.Sprintf("<task-%d@%s>", taskID, mail.GetMailDomain())
|
|
}
|
|
|
|
// ReminderDueNotification represents a ReminderDueNotification notification
|
|
type ReminderDueNotification struct {
|
|
User *user.User `json:"user,omitempty"`
|
|
Task *Task `json:"task"`
|
|
Project *Project `json:"project"`
|
|
TaskReminder *TaskReminder `json:"reminder"`
|
|
}
|
|
|
|
// ToMail returns the mail notification for ReminderDueNotification
|
|
func (n *ReminderDueNotification) ToMail(lang string) *notifications.Mail {
|
|
return notifications.NewMail().
|
|
IncludeLinkToSettings(lang).
|
|
To(n.User.Email).
|
|
Subject(i18n.T(lang, "notifications.task.reminder.subject", n.Task.Title, n.Project.Title)).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
|
|
Line(i18n.T(lang, "notifications.task.reminder.message", notifications.EscapeMarkdown(n.Task.Title), notifications.EscapeMarkdown(n.Project.Title))).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_task"), config.ServicePublicURL.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
|
|
Line(i18n.T(lang, "notifications.common.have_nice_day"))
|
|
}
|
|
|
|
// ToDB returns the ReminderDueNotification notification in a format which can be saved in the db
|
|
func (n *ReminderDueNotification) ToDB() interface{} {
|
|
return &ReminderDueNotification{
|
|
Task: n.Task,
|
|
Project: n.Project,
|
|
}
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *ReminderDueNotification) Name() string {
|
|
return "task.reminder"
|
|
}
|
|
|
|
// ThreadID returns the thread ID for email threading
|
|
func (n *ReminderDueNotification) ThreadID() string {
|
|
return getThreadID(n.Task.ID)
|
|
}
|
|
|
|
// TaskCommentNotification represents a TaskCommentNotification notification
|
|
type TaskCommentNotification struct {
|
|
Doer *user.User `json:"doer"`
|
|
Task *Task `json:"task"`
|
|
Comment *TaskComment `json:"comment"`
|
|
Mentioned bool `json:"mentioned"`
|
|
Project *Project `json:"project"`
|
|
}
|
|
|
|
func (n *TaskCommentNotification) SubjectID() int64 {
|
|
return n.Comment.ID
|
|
}
|
|
|
|
// ToMail returns the mail notification for TaskCommentNotification
|
|
func (n *TaskCommentNotification) ToMail(lang string) *notifications.Mail {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
formattedComment := formatMentionsForEmail(s, n.Comment.Comment)
|
|
|
|
mail := notifications.NewMail().
|
|
Conversational().
|
|
From(n.Doer.GetNameAndFromEmail()).
|
|
Subject(i18n.T(lang, "notifications.task.comment.subject", n.Task.Title, n.Task.GetFullIdentifier()))
|
|
|
|
// Add header line
|
|
action := i18n.T(lang, "notifications.common.actions.left_comment", n.Doer.GetName())
|
|
if n.Mentioned {
|
|
action = i18n.T(lang, "notifications.common.actions.mentioned_you_comment", n.Doer.GetName())
|
|
mail.Subject(i18n.T(lang, "notifications.task.comment.mentioned_subject", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier()))
|
|
}
|
|
|
|
headerLine := notifications.CreateConversationalHeader(
|
|
getDoerAvatarDataURI(n.Doer),
|
|
action,
|
|
n.Task.GetFrontendURL(),
|
|
n.Project.Title,
|
|
n.Task.GetFullIdentifier(),
|
|
n.Task.Title,
|
|
)
|
|
mail.HeaderLine(headerLine)
|
|
|
|
// Add the actual comment content wrapped in a div for consistent spacing
|
|
mail.HTML(formattedComment)
|
|
|
|
return mail.
|
|
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
|
|
IncludeLinkToSettings(lang)
|
|
}
|
|
|
|
// ToDB returns the TaskCommentNotification notification in a format which can be saved in the db
|
|
func (n *TaskCommentNotification) ToDB() interface{} {
|
|
return n
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *TaskCommentNotification) Name() string {
|
|
return "task.comment"
|
|
}
|
|
|
|
// ThreadID returns the thread ID for email threading
|
|
func (n *TaskCommentNotification) ThreadID() string {
|
|
return getThreadID(n.Task.ID)
|
|
}
|
|
|
|
// TaskAssignedNotification represents a TaskAssignedNotification notification
|
|
type TaskAssignedNotification struct {
|
|
Doer *user.User `json:"doer"`
|
|
Task *Task `json:"task"`
|
|
Assignee *user.User `json:"assignee"`
|
|
Target *user.User `json:"-"`
|
|
Project *Project `json:"project"`
|
|
}
|
|
|
|
// ToMail returns the mail notification for TaskAssignedNotification
|
|
func (n *TaskAssignedNotification) ToMail(lang string) *notifications.Mail {
|
|
if n.Target.ID == n.Assignee.ID {
|
|
// Notification to the assignee
|
|
return notifications.NewMail().
|
|
From(n.Doer.GetNameAndFromEmail()).
|
|
Subject(i18n.T(lang, "notifications.task.assigned.subject_to_assignee", n.Task.Title, n.Task.GetFullIdentifier())).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())).
|
|
Line(i18n.T(lang, "notifications.task.assigned.message_to_assignee", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Task.Title))).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
|
|
IncludeLinkToSettings(lang)
|
|
}
|
|
|
|
// Check if the doer assigned the task to themselves
|
|
if n.Doer.ID == n.Assignee.ID {
|
|
return notifications.NewMail().
|
|
From(n.Doer.GetNameAndFromEmail()).
|
|
Subject(i18n.T(lang, "notifications.task.assigned.subject_to_others_self", n.Task.Title, n.Task.GetFullIdentifier(), n.Doer.GetName())).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())).
|
|
Line(i18n.T(lang, "notifications.task.assigned.message_to_others_self", notifications.EscapeMarkdown(n.Doer.GetName()))).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
|
|
IncludeLinkToSettings(lang)
|
|
}
|
|
|
|
// Notification to others about assignment
|
|
return notifications.NewMail().
|
|
From(n.Doer.GetNameAndFromEmail()).
|
|
Subject(i18n.T(lang, "notifications.task.assigned.subject_to_others", n.Task.Title, n.Task.GetFullIdentifier(), n.Assignee.GetName())).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())).
|
|
Line(i18n.T(lang, "notifications.task.assigned.message_to_others", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Assignee.GetName()))).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
|
|
IncludeLinkToSettings(lang)
|
|
}
|
|
|
|
// ToDB returns the TaskAssignedNotification notification in a format which can be saved in the db
|
|
func (n *TaskAssignedNotification) ToDB() interface{} {
|
|
return n
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *TaskAssignedNotification) Name() string {
|
|
return "task.assigned"
|
|
}
|
|
|
|
// ThreadID returns the thread ID for email threading
|
|
func (n *TaskAssignedNotification) ThreadID() string {
|
|
return getThreadID(n.Task.ID)
|
|
}
|
|
|
|
// TaskDeletedNotification represents a TaskDeletedNotification notification
|
|
type TaskDeletedNotification struct {
|
|
Doer *user.User `json:"doer"`
|
|
Task *Task `json:"task"`
|
|
}
|
|
|
|
// ToMail returns the mail notification for TaskDeletedNotification
|
|
func (n *TaskDeletedNotification) ToMail(lang string) *notifications.Mail {
|
|
return notifications.NewMail().
|
|
Subject(i18n.T(lang, "notifications.task.deleted.subject", n.Task.Title, n.Task.GetFullIdentifier())).
|
|
Line(i18n.T(lang, "notifications.task.deleted.message", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Task.Title), notifications.EscapeMarkdown(n.Task.GetFullIdentifier())))
|
|
}
|
|
|
|
// ToDB returns the TaskDeletedNotification notification in a format which can be saved in the db
|
|
func (n *TaskDeletedNotification) ToDB() interface{} {
|
|
return n
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *TaskDeletedNotification) Name() string {
|
|
return "task.deleted"
|
|
}
|
|
|
|
// ThreadID returns the thread ID for email threading
|
|
func (n *TaskDeletedNotification) ThreadID() string {
|
|
return getThreadID(n.Task.ID)
|
|
}
|
|
|
|
// ProjectCreatedNotification represents a ProjectCreatedNotification notification
|
|
type ProjectCreatedNotification struct {
|
|
Doer *user.User `json:"doer"`
|
|
Project *Project `json:"project"`
|
|
}
|
|
|
|
// ToMail returns the mail notification for ProjectCreatedNotification
|
|
func (n *ProjectCreatedNotification) ToMail(lang string) *notifications.Mail {
|
|
return notifications.NewMail().
|
|
Subject(i18n.T(lang, "notifications.project.created", n.Doer.GetName(), n.Project.Title)).
|
|
Line(i18n.T(lang, "notifications.project.created", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Project.Title))).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_project"), config.ServicePublicURL.GetString()+"projects/")
|
|
}
|
|
|
|
// ToDB returns the ProjectCreatedNotification notification in a format which can be saved in the db
|
|
func (n *ProjectCreatedNotification) ToDB() interface{} {
|
|
return n
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *ProjectCreatedNotification) Name() string {
|
|
return "project.created"
|
|
}
|
|
|
|
// TeamMemberAddedNotification represents a TeamMemberAddedNotification notification
|
|
type TeamMemberAddedNotification struct {
|
|
Member *user.User `json:"member"`
|
|
Doer *user.User `json:"doer"`
|
|
Team *Team `json:"team"`
|
|
}
|
|
|
|
// ToMail returns the mail notification for TeamMemberAddedNotification
|
|
func (n *TeamMemberAddedNotification) ToMail(lang string) *notifications.Mail {
|
|
return notifications.NewMail().
|
|
Subject(i18n.T(lang, "notifications.team.member_added.subject", n.Doer.GetName(), n.Team.Name)).
|
|
From(n.Doer.GetNameAndFromEmail()).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.Member.GetName())).
|
|
Line(i18n.T(lang, "notifications.team.member_added.message", notifications.EscapeMarkdown(n.Doer.GetName()), notifications.EscapeMarkdown(n.Team.Name))).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_team"), config.ServicePublicURL.GetString()+"teams/"+strconv.FormatInt(n.Team.ID, 10)+"/edit")
|
|
}
|
|
|
|
// ToDB returns the TeamMemberAddedNotification notification in a format which can be saved in the db
|
|
func (n *TeamMemberAddedNotification) ToDB() interface{} {
|
|
return n
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *TeamMemberAddedNotification) Name() string {
|
|
return "team.member.added"
|
|
}
|
|
|
|
func getOverdueSinceString(until time.Duration, language string) (overdueSince string) {
|
|
if until == 0 {
|
|
return i18n.T(language, "notifications.task.overdue.overdue_now")
|
|
}
|
|
|
|
return i18n.T(language, "notifications.task.overdue.overdue_since", utils.HumanizeDuration(until, language))
|
|
}
|
|
|
|
// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification
|
|
type UndoneTaskOverdueNotification struct {
|
|
User *user.User
|
|
Task *Task
|
|
Project *Project
|
|
}
|
|
|
|
// ToMail returns the mail notification for UndoneTaskOverdueNotification
|
|
func (n *UndoneTaskOverdueNotification) ToMail(lang string) *notifications.Mail {
|
|
until := time.Until(n.Task.DueDate).Round(1*time.Hour) * -1
|
|
return notifications.NewMail().
|
|
IncludeLinkToSettings(lang).
|
|
Subject(i18n.T(lang, "notifications.task.overdue.subject", n.Task.Title, n.Project.Title)).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
|
|
Line(i18n.T(lang, "notifications.task.overdue.message", notifications.EscapeMarkdown(n.Task.Title), notifications.EscapeMarkdown(n.Project.Title), getOverdueSinceString(until, n.User.Language))).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_task"), config.ServicePublicURL.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
|
|
Line(i18n.T(lang, "notifications.common.have_nice_day"))
|
|
}
|
|
|
|
// ToDB returns the UndoneTaskOverdueNotification notification in a format which can be saved in the db
|
|
func (n *UndoneTaskOverdueNotification) ToDB() interface{} {
|
|
return nil
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *UndoneTaskOverdueNotification) Name() string {
|
|
return "task.undone.overdue"
|
|
}
|
|
|
|
// ThreadID returns the thread ID for email threading
|
|
func (n *UndoneTaskOverdueNotification) ThreadID() string {
|
|
return getThreadID(n.Task.ID)
|
|
}
|
|
|
|
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
|
|
type UndoneTasksOverdueNotification struct {
|
|
User *user.User
|
|
Tasks map[int64]*Task
|
|
Projects map[int64]*Project
|
|
}
|
|
|
|
// ToMail returns the mail notification for UndoneTasksOverdueNotification
|
|
func (n *UndoneTasksOverdueNotification) ToMail(lang string) *notifications.Mail {
|
|
|
|
sortedTasks := make([]*Task, 0, len(n.Tasks))
|
|
for _, task := range n.Tasks {
|
|
sortedTasks = append(sortedTasks, task)
|
|
}
|
|
|
|
sort.Slice(sortedTasks, func(i, j int) bool {
|
|
return sortedTasks[i].DueDate.Before(sortedTasks[j].DueDate)
|
|
})
|
|
|
|
overdueLine := ""
|
|
for _, task := range sortedTasks {
|
|
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
|
|
overdueLine += `* [` + notifications.EscapeMarkdown(task.Title) + `](` + config.ServicePublicURL.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `) (` + notifications.EscapeMarkdown(n.Projects[task.ProjectID].Title) + `), ` + i18n.T(lang, "notifications.task.overdue.overdue", getOverdueSinceString(until, n.User.Language)) + "\n"
|
|
}
|
|
|
|
return notifications.NewMail().
|
|
IncludeLinkToSettings(lang).
|
|
Subject(i18n.T(lang, "notifications.task.overdue.multiple_subject")).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
|
|
Line(i18n.T(lang, "notifications.task.overdue.multiple_message")).
|
|
Line(overdueLine).
|
|
Action(i18n.T(lang, "notifications.common.actions.open_vikunja"), config.ServicePublicURL.GetString()).
|
|
Line(i18n.T(lang, "notifications.common.have_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"
|
|
}
|
|
|
|
// UserMentionedInTaskNotification represents a UserMentionedInTaskNotification notification
|
|
type UserMentionedInTaskNotification struct {
|
|
Doer *user.User `json:"doer"`
|
|
Task *Task `json:"task"`
|
|
IsNew bool `json:"is_new"`
|
|
Project *Project `json:"project"`
|
|
}
|
|
|
|
func (n *UserMentionedInTaskNotification) SubjectID() int64 {
|
|
return n.Task.ID
|
|
}
|
|
|
|
// ToMail returns the mail notification for UserMentionedInTaskNotification
|
|
func (n *UserMentionedInTaskNotification) ToMail(lang string) *notifications.Mail {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
formattedDescription := formatMentionsForEmail(s, n.Task.Description)
|
|
|
|
var subject string
|
|
if n.IsNew {
|
|
subject = i18n.T(lang, "notifications.task.mentioned.subject_new", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier())
|
|
} else {
|
|
subject = i18n.T(lang, "notifications.task.mentioned.subject", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier())
|
|
}
|
|
|
|
mail := notifications.NewMail().
|
|
Conversational().
|
|
From(n.Doer.GetNameAndFromEmail()).
|
|
Subject(subject)
|
|
|
|
// Add header line
|
|
action := i18n.T(lang, "notifications.common.actions.mentioned_you", n.Doer.GetName())
|
|
if n.IsNew {
|
|
action = i18n.T(lang, "notifications.common.actions.mentioned_you_new_task", n.Doer.GetName())
|
|
}
|
|
|
|
headerLine := notifications.CreateConversationalHeader(
|
|
getDoerAvatarDataURI(n.Doer),
|
|
action,
|
|
n.Task.GetFrontendURL(),
|
|
n.Project.Title,
|
|
n.Task.GetFullIdentifier(),
|
|
n.Task.Title,
|
|
)
|
|
mail.HeaderLine(headerLine)
|
|
|
|
if formattedDescription != "" {
|
|
mail.HTML(formattedDescription)
|
|
}
|
|
|
|
return mail.
|
|
Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()).
|
|
IncludeLinkToSettings(lang)
|
|
}
|
|
|
|
// ToDB returns the UserMentionedInTaskNotification notification in a format which can be saved in the db
|
|
func (n *UserMentionedInTaskNotification) ToDB() interface{} {
|
|
return n
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *UserMentionedInTaskNotification) Name() string {
|
|
return "task.mentioned"
|
|
}
|
|
|
|
// ThreadID returns the thread ID for email threading
|
|
func (n *UserMentionedInTaskNotification) ThreadID() string {
|
|
return getThreadID(n.Task.ID)
|
|
}
|
|
|
|
// DataExportReadyNotification represents a DataExportReadyNotification notification
|
|
type DataExportReadyNotification struct {
|
|
User *user.User `json:"user"`
|
|
}
|
|
|
|
// ToMail returns the mail notification for DataExportReadyNotification
|
|
func (n *DataExportReadyNotification) ToMail(lang string) *notifications.Mail {
|
|
return notifications.NewMail().
|
|
Subject(i18n.T(lang, "notifications.data_export.ready.subject")).
|
|
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
|
|
Line(i18n.T(lang, "notifications.data_export.ready.message")).
|
|
Action(i18n.T(lang, "notifications.common.actions.download"), config.ServicePublicURL.GetString()+"user/export/download").
|
|
Line(i18n.T(lang, "notifications.data_export.ready.availability")).
|
|
Line(i18n.T(lang, "notifications.common.have_nice_day"))
|
|
}
|
|
|
|
// ToDB returns the DataExportReadyNotification notification in a format which can be saved in the db
|
|
func (n *DataExportReadyNotification) ToDB() interface{} {
|
|
return nil
|
|
}
|
|
|
|
// Name returns the name of the notification
|
|
func (n *DataExportReadyNotification) Name() string {
|
|
return "data.export.ready"
|
|
}
|