2021-04-11 15:08:43 +00:00
// 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 models
import (
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/cron"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/notifications"
2022-06-12 19:24:16 +00:00
"code.vikunja.io/api/pkg/user"
2021-04-11 15:08:43 +00:00
"code.vikunja.io/api/pkg/utils"
2022-06-12 19:24:16 +00:00
2021-04-11 15:08:43 +00:00
"xorm.io/builder"
"xorm.io/xorm"
)
2022-06-12 19:24:16 +00:00
func getUndoneOverdueTasks ( s * xorm . Session , now time . Time ) ( usersWithTasks map [ int64 ] * userWithTasks , err error ) {
2022-06-16 14:20:26 +00:00
now = utils . GetTimeWithoutSeconds ( now )
2022-06-12 19:24:16 +00:00
nextMinute := now . Add ( 1 * time . Minute )
2021-04-11 15:08:43 +00:00
var tasks [ ] * Task
err = s .
2022-12-23 17:55:57 +00:00
Where ( "due_date is not null AND due_date < ? AND projects.is_archived = false AND namespaces.is_archived = false" , nextMinute . Add ( time . Hour * 14 ) . Format ( dbTimeFormat ) ) .
Join ( "LEFT" , "projects" , "projects.id = tasks.project_id" ) .
Join ( "LEFT" , "namespaces" , "projects.namespace_id = namespaces.id" ) .
2021-04-11 15:08:43 +00:00
And ( "done = false" ) .
Find ( & tasks )
if err != nil {
return
}
2022-06-12 19:24:16 +00:00
if len ( tasks ) == 0 {
return
}
var taskIDs [ ] int64
2021-04-11 15:08:43 +00:00
for _ , task := range tasks {
taskIDs = append ( taskIDs , task . ID )
}
2022-06-12 19:24:16 +00:00
users , err := getTaskUsersForTasks ( s , taskIDs , builder . Eq { "users.overdue_tasks_reminders_enabled" : true } )
if err != nil {
return
}
if len ( users ) == 0 {
return
}
uts := make ( map [ int64 ] * userWithTasks )
tzs := make ( map [ string ] * time . Location )
for _ , t := range users {
if t . User . Timezone == "" {
t . User . Timezone = config . GetTimeZone ( ) . String ( )
}
tz , exists := tzs [ t . User . Timezone ]
if ! exists {
tz , err = time . LoadLocation ( t . User . Timezone )
if err != nil {
return
}
tzs [ t . User . Timezone ] = tz
}
2022-11-13 16:07:01 +00:00
// If it is time for that current user, add the task to their project of overdue tasks
2022-06-16 14:20:26 +00:00
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 )
2022-06-12 19:24:16 +00:00
isTimeForReminder := overdueMailTime . After ( now ) || overdueMailTime . Equal ( now . In ( tz ) )
2022-06-16 14:20:26 +00:00
wasTimeForReminder := overdueMailTime . Before ( nextMinute )
2022-06-12 19:24:16 +00:00
taskIsOverdueInUserTimezone := overdueMailTime . After ( t . Task . DueDate . In ( tz ) )
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
_ , exists := uts [ t . User . ID ]
if ! exists {
uts [ t . User . ID ] = & userWithTasks {
user : t . User ,
2022-09-30 16:35:40 +00:00
tasks : make ( map [ int64 ] * Task ) ,
2022-06-12 19:24:16 +00:00
}
}
2022-09-30 16:35:40 +00:00
uts [ t . User . ID ] . tasks [ t . Task . ID ] = t . Task
2022-06-12 19:24:16 +00:00
}
}
return uts , nil
2021-04-11 15:08:43 +00:00
}
2021-04-18 13:32:02 +00:00
type userWithTasks struct {
user * user . User
2022-09-30 16:35:40 +00:00
tasks map [ int64 ] * Task
2021-04-18 13:32:02 +00:00
}
2021-04-11 15:08:43 +00:00
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
func RegisterOverdueReminderCron ( ) {
if ! config . ServiceEnableEmailReminders . GetBool ( ) {
return
}
if ! config . MailerEnabled . GetBool ( ) {
log . Info ( "Mailer is disabled, not sending overdue per mail" )
return
}
2022-06-12 19:24:16 +00:00
err := cron . Schedule ( "* * * * *" , func ( ) {
2021-04-11 15:08:43 +00:00
s := db . NewSession ( )
defer s . Close ( )
now := time . Now ( )
2022-06-12 19:24:16 +00:00
uts , err := getUndoneOverdueTasks ( s , now )
2021-04-11 15:08:43 +00:00
if err != nil {
2022-06-12 19:24:16 +00:00
log . Errorf ( "[Undone Overdue Tasks Reminder] Could not get undone overdue tasks in the next minute: %s" , err )
2021-04-11 15:08:43 +00:00
return
}
2022-06-12 19:24:16 +00:00
log . Debugf ( "[Undone Overdue Tasks Reminder] Sending reminders to %d users" , len ( uts ) )
2021-04-11 15:08:43 +00:00
2021-04-18 13:32:02 +00:00
for _ , ut := range uts {
var n notifications . Notification = & UndoneTasksOverdueNotification {
User : ut . user ,
Tasks : ut . tasks ,
}
if len ( ut . tasks ) == 1 {
2022-12-01 17:41:24 +00:00
// We know there's only one entry in the map so this is actually O(1) and we can use it to get the
// first entry without knowing the key of it.
for _ , t := range ut . tasks {
n = & UndoneTaskOverdueNotification {
User : ut . user ,
Task : t ,
}
2021-04-18 13:32:02 +00:00
}
2021-04-11 15:08:43 +00:00
}
2021-04-18 13:32:02 +00:00
err = notifications . Notify ( ut . user , n )
2021-04-11 15:08:43 +00:00
if err != nil {
2021-04-18 13:32:02 +00:00
log . Errorf ( "[Undone Overdue Tasks Reminder] Could not notify user %d: %s" , ut . user . ID , err )
2021-04-11 15:08:43 +00:00
return
}
2021-04-18 13:32:02 +00:00
log . Debugf ( "[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d" , len ( ut . tasks ) , ut . user . ID )
2021-04-11 15:08:43 +00:00
}
} )
if err != nil {
log . Fatalf ( "Could not register undone overdue tasks reminder cron: %s" , err )
}
}