From d07b284ee34efd4f669c412e56cfec94b6790985 Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 11 Apr 2021 15:08:43 +0000 Subject: [PATCH] Add reminders for overdue tasks (#832) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/832 Co-authored-by: konrad Co-committed-by: konrad --- pkg/db/fixtures/tasks.yml | 9 + pkg/initialize/init.go | 1 + pkg/migration/20210411161337.go | 43 ++ pkg/models/label_task_test.go | 15 +- pkg/models/label_test.go | 64 +-- pkg/models/list_users_test.go | 32 +- pkg/models/namespace_users_test.go | 32 +- pkg/models/notifications.go | 27 + pkg/models/task_collection_test.go | 49 +- pkg/models/task_overdue_reminder.go | 99 ++++ pkg/models/task_overdue_reminder_test.go | 62 +++ pkg/models/task_reminder.go | 35 +- pkg/models/users_list_test.go | 231 ++++---- pkg/routes/api/v1/user_settings.go | 3 + pkg/routes/api/v1/user_show.go | 9 +- pkg/swagger/docs.go | 4 + pkg/swagger/swagger.json | 4 + pkg/swagger/swagger.yaml | 659 ++++++++++++++++------- pkg/user/user.go | 10 +- pkg/utils/time.go | 32 ++ 20 files changed, 992 insertions(+), 428 deletions(-) create mode 100644 pkg/migration/20210411161337.go create mode 100644 pkg/models/task_overdue_reminder.go create mode 100644 pkg/models/task_overdue_reminder_test.go create mode 100644 pkg/utils/time.go diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml index 73d392b6..b1203561 100644 --- a/pkg/db/fixtures/tasks.yml +++ b/pkg/db/fixtures/tasks.yml @@ -346,3 +346,12 @@ index: 2 created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 +- id: 38 + title: 'task #37 done with due date' + done: true + created_by_id: 1 + list_id: 22 + index: 2 + created: 2018-12-01 01:12:04 + updated: 2018-12-01 01:12:04 + due_date: 2018-10-30 22:25:24 diff --git a/pkg/initialize/init.go b/pkg/initialize/init.go index 2e820672..32e067ac 100644 --- a/pkg/initialize/init.go +++ b/pkg/initialize/init.go @@ -93,6 +93,7 @@ func FullInit() { // Start the cron cron.Init() models.RegisterReminderCron() + models.RegisterOverdueReminderCron() // Start processing events go func() { diff --git a/pkg/migration/20210411161337.go b/pkg/migration/20210411161337.go new file mode 100644 index 00000000..f094df9e --- /dev/null +++ b/pkg/migration/20210411161337.go @@ -0,0 +1,43 @@ +// 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 . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type users20210411161337 struct { + OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"` +} + +func (users20210411161337) TableName() string { + return "users" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210411161337", + Description: "Add overdue notifications enabled setting to users", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(users20210411161337{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/label_task_test.go b/pkg/models/label_task_test.go index b3154c6f..7bd08213 100644 --- a/pkg/models/label_task_test.go +++ b/pkg/models/label_task_test.go @@ -69,13 +69,14 @@ func TestLabelTask_ReadAll(t *testing.T) { Updated: testUpdatedTime, CreatedByID: 2, CreatedBy: &user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 2, + Username: "user2", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, }, }, }, diff --git a/pkg/models/label_test.go b/pkg/models/label_test.go index 54949700..9666695c 100644 --- a/pkg/models/label_test.go +++ b/pkg/models/label_test.go @@ -48,14 +48,15 @@ func TestLabel_ReadAll(t *testing.T) { page int } user1 := &user.User{ - ID: 1, - Username: "user1", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 1, + Username: "user1", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } tests := []struct { name string @@ -98,13 +99,14 @@ func TestLabel_ReadAll(t *testing.T) { Updated: testUpdatedTime, CreatedByID: 2, CreatedBy: &user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 2, + Username: "user2", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, }, }, }, @@ -161,14 +163,15 @@ func TestLabel_ReadOne(t *testing.T) { Rights web.Rights } user1 := &user.User{ - ID: 1, - Username: "user1", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 1, + Username: "user1", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } tests := []struct { name string @@ -222,13 +225,14 @@ func TestLabel_ReadOne(t *testing.T) { Title: "Label #4 - visible via other task", CreatedByID: 2, CreatedBy: &user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 2, + Username: "user2", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, }, Created: testCreatedTime, Updated: testUpdatedTime, diff --git a/pkg/models/list_users_test.go b/pkg/models/list_users_test.go index d7b1620d..f36858b2 100644 --- a/pkg/models/list_users_test.go +++ b/pkg/models/list_users_test.go @@ -177,26 +177,28 @@ func TestListUser_ReadAll(t *testing.T) { want: []*UserWithRight{ { User: user.User{ - ID: 1, - Username: "user1", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 1, + Username: "user1", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, }, Right: RightRead, }, { User: user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 2, + Username: "user2", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, }, Right: RightRead, }, diff --git a/pkg/models/namespace_users_test.go b/pkg/models/namespace_users_test.go index d3dcf145..f9fe51d9 100644 --- a/pkg/models/namespace_users_test.go +++ b/pkg/models/namespace_users_test.go @@ -176,26 +176,28 @@ func TestNamespaceUser_ReadAll(t *testing.T) { want: []*UserWithRight{ { User: user.User{ - ID: 1, - Username: "user1", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 1, + Username: "user1", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, }, Right: RightRead, }, { User: user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 2, + Username: "user2", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, }, Right: RightRead, }, diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index 46e0dd4e..ddb0190c 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -20,6 +20,7 @@ import ( "bufio" "strconv" "strings" + "time" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/notifications" @@ -184,3 +185,29 @@ func (n *TeamMemberAddedNotification) ToDB() interface{} { func (n *TeamMemberAddedNotification) Name() string { return "team.member.added" } + +// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification +type UndoneTaskOverdueNotification struct { + User *user.User + Task *Task +} + +// ToMail returns the mail notification for UndoneTaskOverdueNotification +func (n *UndoneTaskOverdueNotification) ToMail() *notifications.Mail { + return notifications.NewMail(). + Subject(`Task "`+n.Task.Title+`" is overdue`). + Greeting("Hi "+n.User.GetName()+","). + Line(`This is a friendly reminder of the task "`+n.Task.Title+`" which is overdue since `+time.Until(n.Task.DueDate).String()+` and not yet done.`). + Action("Open Task", config.ServiceFrontendurl.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)). + Line("Have a 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" +} diff --git a/pkg/models/task_collection_test.go b/pkg/models/task_collection_test.go index f195cb12..fc163ab8 100644 --- a/pkg/models/task_collection_test.go +++ b/pkg/models/task_collection_test.go @@ -31,33 +31,36 @@ import ( func TestTaskCollection_ReadAll(t *testing.T) { // Dummy users user1 := &user.User{ - ID: 1, - Username: "user1", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 1, + Username: "user1", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } user2 := &user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 2, + Username: "user2", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } user6 := &user.User{ - ID: 6, - Username: "user6", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - IsActive: true, - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 6, + Username: "user6", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + IsActive: true, + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } linkShareUser2 := &user.User{ ID: -2, diff --git a/pkg/models/task_overdue_reminder.go b/pkg/models/task_overdue_reminder.go new file mode 100644 index 00000000..8ab72460 --- /dev/null +++ b/pkg/models/task_overdue_reminder.go @@ -0,0 +1,99 @@ +// 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 . + +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" + "code.vikunja.io/api/pkg/utils" + "xorm.io/builder" + "xorm.io/xorm" +) + +func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, err error) { + now = utils.GetTimeWithoutNanoSeconds(now) + + var tasks []*Task + err = s. + Where("due_date is not null and due_date < ?", now.Format(dbTimeFormat)). + And("done = false"). + Find(&tasks) + if err != nil { + return + } + + for _, task := range tasks { + taskIDs = append(taskIDs, task.ID) + } + + return +} + +// 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 + } + + err := cron.Schedule("0 8 * * *", func() { + s := db.NewSession() + defer s.Close() + + now := time.Now() + taskIDs, err := getUndoneOverdueTasks(s, now) + if err != nil { + log.Errorf("[Undone Overdue Tasks Reminder] Could not get tasks with reminders in the next minute: %s", err) + return + } + + users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true}) + if err != nil { + log.Errorf("[Undone Overdue Tasks Reminder] Could not get task users to send them reminders: %s", err) + return + } + + log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users)) + + for _, u := range users { + n := &UndoneTaskOverdueNotification{ + User: u.User, + Task: u.Task, + } + + err = notifications.Notify(u.User, n) + if err != nil { + log.Errorf("[Undone Overdue Tasks Reminder] Could not notify user %d: %s", u.User.ID, err) + return + } + + log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID) + } + }) + if err != nil { + log.Fatalf("Could not register undone overdue tasks reminder cron: %s", err) + } +} diff --git a/pkg/models/task_overdue_reminder_test.go b/pkg/models/task_overdue_reminder_test.go new file mode 100644 index 00000000..9845ed6f --- /dev/null +++ b/pkg/models/task_overdue_reminder_test.go @@ -0,0 +1,62 @@ +// 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 . + +package models + +import ( + "testing" + "time" + + "code.vikunja.io/api/pkg/db" + "github.com/stretchr/testify/assert" +) + +func TestGetUndoneOverDueTasks(t *testing.T) { + t.Run("no undone tasks", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z") + assert.NoError(t, err) + taskIDs, err := getUndoneOverdueTasks(s, now) + assert.NoError(t, err) + assert.Len(t, taskIDs, 0) + }) + t.Run("undone overdue", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z") + assert.NoError(t, err) + taskIDs, err := getUndoneOverdueTasks(s, now) + assert.NoError(t, err) + assert.Len(t, taskIDs, 1) + assert.Equal(t, int64(6), taskIDs[0]) + }) + t.Run("done overdue", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z") + assert.NoError(t, err) + taskIDs, err := getUndoneOverdueTasks(s, now) + assert.NoError(t, err) + assert.Len(t, taskIDs, 0) + }) +} diff --git a/pkg/models/task_reminder.go b/pkg/models/task_reminder.go index f44c7065..19365ed1 100644 --- a/pkg/models/task_reminder.go +++ b/pkg/models/task_reminder.go @@ -19,6 +19,9 @@ package models import ( "time" + "code.vikunja.io/api/pkg/utils" + "xorm.io/builder" + "code.vikunja.io/api/pkg/notifications" "code.vikunja.io/api/pkg/db" @@ -48,7 +51,9 @@ type taskUser struct { User *user.User `xorm:"extends"` } -func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUser, err error) { +const dbTimeFormat = `2006-01-02 15:04:05` + +func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (taskUsers []*taskUser, err error) { if len(taskIDs) == 0 { return } @@ -59,7 +64,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs Select("users.id, users.username, users.email, users.name"). Join("LEFT", "tasks", "tasks.created_by_id = users.id"). In("tasks.id", taskIDs). - Where("users.email_reminders_enabled = true"). + Where(cond). GroupBy("tasks.id, users.id, users.username, users.email, users.name"). Find(&creators) if err != nil { @@ -84,15 +89,18 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs }) } - assignees, err := getRawTaskAssigneesForTasks(s, taskIDs) + var assignees []*TaskAssigneeWithUser + err = s.Table("task_assignees"). + Select("task_id, users.*"). + In("task_id", taskIDs). + Join("INNER", "users", "task_assignees.user_id = users.id"). + Where(cond). + Find(&assignees) if err != nil { return } for _, assignee := range assignees { - if !assignee.EmailRemindersEnabled { // Can't filter that through a query directly since we're using another function - continue - } taskUsers = append(taskUsers, &taskUser{ Task: taskMap[assignee.TaskID], User: &assignee.User, @@ -103,13 +111,8 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs } func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskIDs []int64, err error) { + now = utils.GetTimeWithoutNanoSeconds(now) - tz := config.GetTimeZone() - const dbFormat = `2006-01-02 15:04:05` - - // 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. - now = time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), 0, 0, now.Location()).In(tz) nextMinute := now.Add(1 * time.Minute) log.Debugf("[Task Reminder Cron] Looking for reminders between %s and %s to send...", now, nextMinute) @@ -117,7 +120,7 @@ func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskI reminders := []*TaskReminder{} err = s. Join("INNER", "tasks", "tasks.id = task_reminders.task_id"). - Where("reminder >= ? and reminder < ?", now.Format(dbFormat), nextMinute.Format(dbFormat)). + Where("reminder >= ? and reminder < ?", now.Format(dbTimeFormat), nextMinute.Format(dbTimeFormat)). And("tasks.done = false"). Find(&reminders) if err != nil { @@ -154,9 +157,9 @@ func RegisterReminderCron() { log.Debugf("[Task Reminder Cron] Timezone is %s", tz) - s := db.NewSession() - err := cron.Schedule("* * * * *", func() { + s := db.NewSession() + defer s.Close() now := time.Now() taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now) @@ -169,7 +172,7 @@ func RegisterReminderCron() { return } - users, err := getTaskUsersForTasks(s, taskIDs) + users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true}) if err != nil { log.Errorf("[Task Reminder Cron] Could not get task users to send them reminders: %s", err) return diff --git a/pkg/models/users_list_test.go b/pkg/models/users_list_test.go index 5ea9889c..dc4c17ea 100644 --- a/pkg/models/users_list_test.go +++ b/pkg/models/users_list_test.go @@ -26,139 +26,152 @@ import ( func TestListUsersFromList(t *testing.T) { testuser1 := &user.User{ - ID: 1, - Username: "user1", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 1, + Username: "user1", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser2 := &user.User{ - ID: 2, - Username: "user2", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 2, + Username: "user2", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser3 := &user.User{ - ID: 3, - Username: "user3", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - PasswordResetToken: "passwordresettesttoken", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 3, + Username: "user3", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + PasswordResetToken: "passwordresettesttoken", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser4 := &user.User{ - ID: 4, - Username: "user4", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: false, - EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 4, + Username: "user4", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: false, + EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser5 := &user.User{ - ID: 5, - Username: "user5", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: false, - EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael", - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 5, + Username: "user5", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: false, + EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael", + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser6 := &user.User{ - ID: 6, - Username: "user6", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 6, + Username: "user6", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser7 := &user.User{ - ID: 7, - Username: "user7", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - DiscoverableByEmail: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 7, + Username: "user7", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + DiscoverableByEmail: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser8 := &user.User{ - ID: 8, - Username: "user8", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 8, + Username: "user8", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser9 := &user.User{ - ID: 9, - Username: "user9", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 9, + Username: "user9", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser10 := &user.User{ - ID: 10, - Username: "user10", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 10, + Username: "user10", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser11 := &user.User{ - ID: 11, - Username: "user11", - Name: "Some one else", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 11, + Username: "user11", + Name: "Some one else", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser12 := &user.User{ - ID: 12, - Username: "user12", - Name: "Name with spaces", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - DiscoverableByName: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 12, + Username: "user12", + Name: "Name with spaces", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + DiscoverableByName: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } testuser13 := &user.User{ - ID: 13, - Username: "user13", - Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", - IsActive: true, - Issuer: "local", - EmailRemindersEnabled: true, - Created: testCreatedTime, - Updated: testUpdatedTime, + ID: 13, + Username: "user13", + Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.", + IsActive: true, + Issuer: "local", + EmailRemindersEnabled: true, + OverdueTasksRemindersEnabled: true, + Created: testCreatedTime, + Updated: testUpdatedTime, } type args struct { diff --git a/pkg/routes/api/v1/user_settings.go b/pkg/routes/api/v1/user_settings.go index 8c557196..e5255efe 100644 --- a/pkg/routes/api/v1/user_settings.go +++ b/pkg/routes/api/v1/user_settings.go @@ -43,6 +43,8 @@ type UserSettings struct { DiscoverableByName bool `json:"discoverable_by_name"` // If true, the user can be found when searching for their exact email. 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"` } // GetUserAvatarProvider returns the currently set user avatar @@ -167,6 +169,7 @@ func UpdateGeneralUserSettings(c echo.Context) error { user.EmailRemindersEnabled = us.EmailRemindersEnabled user.DiscoverableByEmail = us.DiscoverableByEmail user.DiscoverableByName = us.DiscoverableByName + user.OverdueTasksRemindersEnabled = us.OverdueTasksRemindersEnabled _, err = user2.UpdateUser(s, user) if err != nil { diff --git a/pkg/routes/api/v1/user_show.go b/pkg/routes/api/v1/user_show.go index 266281a4..601962d6 100644 --- a/pkg/routes/api/v1/user_show.go +++ b/pkg/routes/api/v1/user_show.go @@ -63,10 +63,11 @@ func UserShow(c echo.Context) error { us := &userWithSettings{ User: *u, Settings: &UserSettings{ - Name: u.Name, - EmailRemindersEnabled: u.EmailRemindersEnabled, - DiscoverableByName: u.DiscoverableByName, - DiscoverableByEmail: u.DiscoverableByEmail, + Name: u.Name, + EmailRemindersEnabled: u.EmailRemindersEnabled, + DiscoverableByName: u.DiscoverableByName, + DiscoverableByEmail: u.DiscoverableByEmail, + OverdueTasksRemindersEnabled: u.OverdueTasksRemindersEnabled, }, } diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 01e0cfe6..74c2dcb9 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -8570,6 +8570,10 @@ var doc = `{ "name": { "description": "The new name of the current user.", "type": "string" + }, + "overdue_tasks_reminders_enabled": { + "description": "If enabled, the user will get an email for their overdue tasks each morning.", + "type": "boolean" } } }, diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index de2de137..d453eeb5 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -8553,6 +8553,10 @@ "name": { "description": "The new name of the current user.", "type": "string" + }, + "overdue_tasks_reminders_enabled": { + "description": "If enabled, the user will get an email for their overdue tasks each morning.", + "type": "boolean" } } }, diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index defd34ea..a545d217 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -10,7 +10,8 @@ definitions: id: type: string info: - description: This can be used to supply extra information from an image provider to clients + description: This can be used to supply extra information from an image provider + to clients type: object thumb: type: string @@ -52,7 +53,8 @@ definitions: models.Bucket: properties: created: - description: A timestamp when this bucket was created. You cannot change this value. + description: A timestamp when this bucket was created. You cannot change this + value. type: string created_by: $ref: '#/definitions/user.User' @@ -68,7 +70,8 @@ definitions: type: string type: array filter_concat: - description: The way all filter conditions are concatenated together, can be either "and" or "or"., + description: The way all filter conditions are concatenated together, can + be either "and" or "or"., type: string filter_include_nulls: description: If set to true, the result will also include null values @@ -82,7 +85,9 @@ definitions: description: The unique, numeric id of this bucket. type: integer is_done_bucket: - description: If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket. + description: If this bucket is the "done bucket". All tasks moved into this + bucket will automatically marked as done. All tasks marked as done from + elsewhere will be moved into this bucket. type: boolean limit: description: How many tasks can be at the same time on this board max @@ -91,12 +96,14 @@ definitions: description: The list this bucket belongs to. type: integer order_by: - description: The query parameter to order the items by. This can be either asc or desc, with asc being the default. + description: The query parameter to order the items by. This can be either + asc or desc, with asc being the default. items: type: string type: array sort_by: - description: The query parameter to sort by. This is for ex. done, priority, etc. + description: The query parameter to sort by. This is for ex. done, priority, + etc. items: type: string type: array @@ -110,7 +117,8 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this bucket was last updated. You cannot change this value. + description: A timestamp when this bucket was last updated. You cannot change + this value. type: string type: object models.BulkAssignees: @@ -137,7 +145,8 @@ definitions: description: BucketID is the ID of the kanban bucket this task belongs to. type: integer created: - description: A timestamp when this task was created. You cannot change this value. + description: A timestamp when this task was created. You cannot change this + value. type: string created_by: $ref: '#/definitions/user.User' @@ -165,13 +174,15 @@ definitions: description: The unique, numeric id of this task. type: integer identifier: - description: The task identifier, based on the list identifier and the task's index + description: The task identifier, based on the list identifier and the task's + index type: string index: description: The task index, calculated per list type: integer is_favorite: - description: True if a task is a favorite task. Favorite tasks show up in a separate "Important" list + description: True if a task is a favorite task. Favorite tasks show up in + a separate "Important" list type: boolean labels: description: An array of labels which are associated with this task. @@ -194,21 +205,26 @@ definitions: which also leaves a lot of room for rearranging and sorting later. type: number priority: - description: The task priority. Can be anything you want, it is possible to sort by this later. + description: The task priority. Can be anything you want, it is possible to + sort by this later. type: integer related_tasks: $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: - description: An array of datetimes when the user wants to be reminded of the task. + description: An array of datetimes when the user wants to be reminded of the + task. items: type: string type: array repeat_after: - description: An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount. + description: An amount in seconds this task repeats itself. If this is set, + when marking the task as done, it will mark itself as "undone" and then + increase all remindes and the due date by its amount. type: integer repeat_from_current_date: - description: If specified, a repeating task will repeat from the current date rather than the last set date. + description: If specified, a repeating task will repeat from the current date + rather than the last set date. type: boolean start_date: description: When this task starts. @@ -229,13 +245,15 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this task was last updated. You cannot change this value. + description: A timestamp when this task was last updated. You cannot change + this value. type: string type: object models.DatabaseNotifications: properties: created: - description: A timestamp when this notification was created. You cannot change this value. + description: A timestamp when this notification was created. You cannot change + this value. type: string id: description: The unique, numeric id of this notification. @@ -252,13 +270,15 @@ definitions: True is read, false is unread. type: boolean read_at: - description: When this notification is marked as read, this will be updated with the current timestamp. + description: When this notification is marked as read, this will be updated + with the current timestamp. type: string type: object models.Label: properties: created: - description: A timestamp when this label was created. You cannot change this value. + description: A timestamp when this label was created. You cannot change this + value. type: string created_by: $ref: '#/definitions/user.User' @@ -274,18 +294,21 @@ definitions: description: The unique, numeric id of this label. type: integer title: - description: The title of the lable. You'll see this one on tasks associated with it. + description: The title of the lable. You'll see this one on tasks associated + with it. maxLength: 250 minLength: 1 type: string updated: - description: A timestamp when this label was last updated. You cannot change this value. + description: A timestamp when this label was last updated. You cannot change + this value. type: string type: object models.LabelTask: properties: created: - description: A timestamp when this task was created. You cannot change this value. + description: A timestamp when this task was created. You cannot change this + value. type: string label_id: description: The label id you want to associate with a task. @@ -302,7 +325,8 @@ definitions: models.LinkSharing: properties: created: - description: A timestamp when this list was shared. You cannot change this value. + description: A timestamp when this list was shared. You cannot change this + value. type: string hash: description: The public id to get this shared list @@ -311,14 +335,17 @@ definitions: description: The ID of the shared thing type: integer name: - description: The name of this link share. All actions someone takes while being authenticated with that link will appear with that name. + description: The name of this link share. All actions someone takes while + being authenticated with that link will appear with that name. type: string password: - description: The password of this link share. You can only set it, not retrieve it after the link share has been created. + description: The password of this link share. You can only set it, not retrieve + it after the link share has been created. type: string right: default: 0 - description: The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. + description: The right this list is shared with. 0 = Read only, 1 = Read & + Write, 2 = Admin. See the docs for more details. maximum: 2 type: integer shared_by: @@ -326,20 +353,25 @@ definitions: description: The user who shared this list sharing_type: default: 0 - description: The kind of this link. 0 = undefined, 1 = without password, 2 = with password. + description: The kind of this link. 0 = undefined, 1 = without password, 2 + = with password. maximum: 2 type: integer updated: - description: A timestamp when this share was last updated. You cannot change this value. + description: A timestamp when this share was last updated. You cannot change + this value. type: string type: object models.List: properties: background_information: - description: Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background + description: Holds extra information about the background set since some background + providers require attribution or similar. If not null, the background can + be accessed at /lists/{listID}/background type: object created: - description: A timestamp when this list was created. You cannot change this value. + description: A timestamp when this list was created. You cannot change this + value. type: string description: description: The description of the list. @@ -360,7 +392,8 @@ definitions: description: Whether or not a list is archived. type: boolean is_favorite: - description: True if a list is a favorite. Favorite lists show up in a separate namespace. + description: True if a list is a favorite. Favorite lists show up in a separate + namespace. type: boolean namespace_id: type: integer @@ -378,7 +411,8 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this list was last updated. You cannot change this value. + description: A timestamp when this list was last updated. You cannot change + this value. type: string type: object models.ListDuplicate: @@ -393,18 +427,21 @@ definitions: models.ListUser: properties: created: - description: A timestamp when this relation was created. You cannot change this value. + description: A timestamp when this relation was created. You cannot change + this value. type: string id: description: The unique, numeric id of this list <-> user relation. type: integer right: default: 0 - description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. + description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = + Admin. See the docs for more details. maximum: 2 type: integer updated: - description: A timestamp when this relation was last updated. You cannot change this value. + description: A timestamp when this relation was last updated. You cannot change + this value. type: string user_id: description: The username. @@ -419,7 +456,8 @@ definitions: models.Namespace: properties: created: - description: A timestamp when this namespace was created. You cannot change this value. + description: A timestamp when this namespace was created. You cannot change + this value. type: string description: description: The description of the namespace @@ -448,24 +486,28 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this namespace was last updated. You cannot change this value. + description: A timestamp when this namespace was last updated. You cannot + change this value. type: string type: object models.NamespaceUser: properties: created: - description: A timestamp when this relation was created. You cannot change this value. + description: A timestamp when this relation was created. You cannot change + this value. type: string id: description: The unique, numeric id of this namespace <-> user relation. type: integer right: default: 0 - description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. + description: The right this user has. 0 = Read only, 1 = Read & Write, 2 = + Admin. See the docs for more details. maximum: 2 type: integer updated: - description: A timestamp when this relation was last updated. You cannot change this value. + description: A timestamp when this relation was last updated. You cannot change + this value. type: string user_id: description: The username. @@ -474,7 +516,8 @@ definitions: models.NamespaceWithLists: properties: created: - description: A timestamp when this namespace was created. You cannot change this value. + description: A timestamp when this namespace was created. You cannot change + this value. type: string description: description: The description of the namespace @@ -507,7 +550,8 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this namespace was last updated. You cannot change this value. + description: A timestamp when this namespace was last updated. You cannot + change this value. type: string type: object models.RelatedTaskMap: @@ -519,7 +563,8 @@ definitions: models.SavedFilter: properties: created: - description: A timestamp when this filter was created. You cannot change this value. + description: A timestamp when this filter was created. You cannot change this + value. type: string description: description: The description of the filter @@ -531,7 +576,8 @@ definitions: description: The unique numeric id of this saved filter type: integer is_favorite: - description: True if the filter is a favorite. Favorite filters show up in a separate namespace together with favorite lists. + description: True if the filter is a favorite. Favorite filters show up in + a separate namespace together with favorite lists. type: boolean owner: $ref: '#/definitions/user.User' @@ -542,13 +588,15 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this filter was last updated. You cannot change this value. + description: A timestamp when this filter was last updated. You cannot change + this value. type: string type: object models.Subscription: properties: created: - description: A timestamp when this subscription was created. You cannot change this value. + description: A timestamp when this subscription was created. You cannot change + this value. type: string entity: type: string @@ -578,7 +626,8 @@ definitions: description: BucketID is the ID of the kanban bucket this task belongs to. type: integer created: - description: A timestamp when this task was created. You cannot change this value. + description: A timestamp when this task was created. You cannot change this + value. type: string created_by: $ref: '#/definitions/user.User' @@ -606,13 +655,15 @@ definitions: description: The unique, numeric id of this task. type: integer identifier: - description: The task identifier, based on the list identifier and the task's index + description: The task identifier, based on the list identifier and the task's + index type: string index: description: The task index, calculated per list type: integer is_favorite: - description: True if a task is a favorite task. Favorite tasks show up in a separate "Important" list + description: True if a task is a favorite task. Favorite tasks show up in + a separate "Important" list type: boolean labels: description: An array of labels which are associated with this task. @@ -635,21 +686,26 @@ definitions: which also leaves a lot of room for rearranging and sorting later. type: number priority: - description: The task priority. Can be anything you want, it is possible to sort by this later. + description: The task priority. Can be anything you want, it is possible to + sort by this later. type: integer related_tasks: $ref: '#/definitions/models.RelatedTaskMap' description: All related tasks, grouped by their relation kind reminder_dates: - description: An array of datetimes when the user wants to be reminded of the task. + description: An array of datetimes when the user wants to be reminded of the + task. items: type: string type: array repeat_after: - description: An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount. + description: An amount in seconds this task repeats itself. If this is set, + when marking the task as done, it will mark itself as "undone" and then + increase all remindes and the due date by its amount. type: integer repeat_from_current_date: - description: If specified, a repeating task will repeat from the current date rather than the last set date. + description: If specified, a repeating task will repeat from the current date + rather than the last set date. type: boolean start_date: description: When this task starts. @@ -665,7 +721,8 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this task was last updated. You cannot change this value. + description: A timestamp when this task was last updated. You cannot change + this value. type: string type: object models.TaskAssginee: @@ -701,7 +758,8 @@ definitions: type: string type: array filter_concat: - description: The way all filter conditions are concatenated together, can be either "and" or "or"., + description: The way all filter conditions are concatenated together, can + be either "and" or "or"., type: string filter_include_nulls: description: If set to true, the result will also include null values @@ -712,12 +770,14 @@ definitions: type: string type: array order_by: - description: The query parameter to order the items by. This can be either asc or desc, with asc being the default. + description: The query parameter to order the items by. This can be either + asc or desc, with asc being the default. items: type: string type: array sort_by: - description: The query parameter to sort by. This is for ex. done, priority, etc. + description: The query parameter to sort by. This is for ex. done, priority, + etc. items: type: string type: array @@ -738,7 +798,8 @@ definitions: models.TaskRelation: properties: created: - description: A timestamp when this label was created. You cannot change this value. + description: A timestamp when this label was created. You cannot change this + value. type: string created_by: $ref: '#/definitions/user.User' @@ -756,7 +817,8 @@ definitions: models.Team: properties: created: - description: A timestamp when this relation was created. You cannot change this value. + description: A timestamp when this relation was created. You cannot change + this value. type: string created_by: $ref: '#/definitions/user.User' @@ -778,71 +840,83 @@ definitions: minLength: 1 type: string updated: - description: A timestamp when this relation was last updated. You cannot change this value. + description: A timestamp when this relation was last updated. You cannot change + this value. type: string type: object models.TeamList: properties: created: - description: A timestamp when this relation was created. You cannot change this value. + description: A timestamp when this relation was created. You cannot change + this value. type: string id: description: The unique, numeric id of this list <-> team relation. type: integer right: default: 0 - description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. + description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = + Admin. See the docs for more details. maximum: 2 type: integer team_id: description: The team id. type: integer updated: - description: A timestamp when this relation was last updated. You cannot change this value. + description: A timestamp when this relation was last updated. You cannot change + this value. type: string type: object models.TeamMember: properties: admin: - description: Whether or not the member is an admin of the team. See the docs for more about what a team admin can do + description: Whether or not the member is an admin of the team. See the docs + for more about what a team admin can do type: boolean created: - description: A timestamp when this relation was created. You cannot change this value. + description: A timestamp when this relation was created. You cannot change + this value. type: string id: description: The unique, numeric id of this team member relation. type: integer username: - description: The username of the member. We use this to prevent automated user id entering. + description: The username of the member. We use this to prevent automated + user id entering. type: string type: object models.TeamNamespace: properties: created: - description: A timestamp when this relation was created. You cannot change this value. + description: A timestamp when this relation was created. You cannot change + this value. type: string id: description: The unique, numeric id of this namespace <-> team relation. type: integer right: default: 0 - description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. + description: The right this team has. 0 = Read only, 1 = Read & Write, 2 = + Admin. See the docs for more details. maximum: 2 type: integer team_id: description: The team id. type: integer updated: - description: A timestamp when this relation was last updated. You cannot change this value. + description: A timestamp when this relation was last updated. You cannot change + this value. type: string type: object models.TeamUser: properties: admin: - description: Whether or not the member is an admin of the team. See the docs for more about what a team admin can do + description: Whether or not the member is an admin of the team. See the docs + for more about what a team admin can do type: boolean created: - description: A timestamp when this task was created. You cannot change this value. + description: A timestamp when this task was created. You cannot change this + value. type: string email: description: The user's email address. @@ -855,7 +929,8 @@ definitions: description: The full name of the user. type: string updated: - description: A timestamp when this task was last updated. You cannot change this value. + description: A timestamp when this task was last updated. You cannot change + this value. type: string username: description: The username of the user. Is always unique. @@ -866,7 +941,8 @@ definitions: models.TeamWithRight: properties: created: - description: A timestamp when this relation was created. You cannot change this value. + description: A timestamp when this relation was created. You cannot change + this value. type: string created_by: $ref: '#/definitions/user.User' @@ -890,13 +966,15 @@ definitions: right: type: integer updated: - description: A timestamp when this relation was last updated. You cannot change this value. + description: A timestamp when this relation was last updated. You cannot change + this value. type: string type: object models.UserWithRight: properties: created: - description: A timestamp when this task was created. You cannot change this value. + description: A timestamp when this task was created. You cannot change this + value. type: string email: description: The user's email address. @@ -911,7 +989,8 @@ definitions: right: type: integer updated: - description: A timestamp when this task was last updated. You cannot change this value. + description: A timestamp when this task was last updated. You cannot change + this value. type: string username: description: The username of the user. Is always unique. @@ -922,7 +1001,8 @@ definitions: notifications.DatabaseNotification: properties: created: - description: A timestamp when this notification was created. You cannot change this value. + description: A timestamp when this notification was created. You cannot change + this value. type: string id: description: The unique, numeric id of this notification. @@ -934,7 +1014,8 @@ definitions: description: The actual content of the notification. type: object read_at: - description: When this notification is marked as read, this will be updated with the current timestamp. + description: When this notification is marked as read, this will be updated + with the current timestamp. type: string type: object openid.Callback: @@ -975,7 +1056,8 @@ definitions: description: The unique, numeric id of this user. type: integer password: - description: The user's password in clear text. Only used when registering the user. + description: The user's password in clear text. Only used when registering + the user. maxLength: 250 minLength: 8 type: string @@ -1030,7 +1112,8 @@ definitions: user.TOTP: properties: enabled: - description: The totp entry will only be enabled after the user verified they have a working totp setup. + description: The totp entry will only be enabled after the user verified they + have a working totp setup. type: boolean secret: type: string @@ -1046,7 +1129,8 @@ definitions: user.User: properties: created: - description: A timestamp when this task was created. You cannot change this value. + description: A timestamp when this task was created. You cannot change this + value. type: string email: description: The user's email address. @@ -1059,7 +1143,8 @@ definitions: description: The full name of the user. type: string updated: - description: A timestamp when this task was last updated. You cannot change this value. + description: A timestamp when this task was last updated. You cannot change + this value. type: string username: description: The username of the user. Is always unique. @@ -1075,7 +1160,8 @@ definitions: v1.UserAvatarProvider: properties: avatar_provider: - description: The avatar provider. Valid types are `gravatar` (uses the user email), `upload`, `initials`, `default`. + description: The avatar provider. Valid types are `gravatar` (uses the user + email), `upload`, `initials`, `default`. type: string type: object v1.UserPassword: @@ -1088,10 +1174,12 @@ definitions: v1.UserSettings: properties: discoverable_by_email: - description: If true, the user can be found when searching for their exact email. + description: If true, the user can be found when searching for their exact + email. type: boolean discoverable_by_name: - description: If true, this user can be found by their name or parts of it when searching for it. + description: If true, this user can be found by their name or parts of it + when searching for it. type: boolean email_reminders_enabled: description: If enabled, sends email reminders of tasks to the user. @@ -1099,6 +1187,10 @@ definitions: name: description: The new name of the current user. type: string + overdue_tasks_reminders_enabled: + description: If enabled, the user will get an email for their overdue tasks + each morning. + type: boolean type: object v1.authInfo: properties: @@ -1236,7 +1328,9 @@ paths: post: consumes: - application/json - description: After a redirect from the OpenID Connect provider to the frontend has been made with the authentication `code`, this endpoint can be used to obtain a jwt token for that user and thus log them in. + description: After a redirect from the OpenID Connect provider to the frontend + has been made with the authentication `code`, this endpoint can be used to + obtain a jwt token for that user and thus log them in. parameters: - description: The openid callback in: body @@ -1296,7 +1390,8 @@ paths: - list /backgrounds/unsplash/image/{image}/thumb: get: - description: Get an unsplash thumbnail image. The thumbnail is cropped to a max width of 200px. **Returns json on error.** + description: Get an unsplash thumbnail image. The thumbnail is cropped to a + max width of 200px. **Returns json on error.** parameters: - description: Unsplash Image ID in: path @@ -1331,7 +1426,8 @@ paths: in: query name: s type: string - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: p type: integer @@ -1479,7 +1575,8 @@ paths: - filter /info: get: - description: Returns the version, frontendurl, motd and various settings of Vikunja + description: Returns the version, frontendurl, motd and various settings of + Vikunja produces: - application/json responses: @@ -1494,13 +1591,16 @@ paths: get: consumes: - application/json - description: Returns all labels which are either created by the user or associated with a task the user has at least read-access to. + description: Returns all labels which are either created by the user or associated + with a task the user has at least read-access to. parameters: - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -1561,7 +1661,8 @@ paths: delete: consumes: - application/json - description: Delete an existing label. The user needs to be the creator of the label to be able to do this. + description: Delete an existing label. The user needs to be the creator of the + label to be able to do this. parameters: - description: Label ID in: path @@ -1629,7 +1730,8 @@ paths: put: consumes: - application/json - description: Update an existing label. The user needs to be the creator of the label to be able to do this. + description: Update an existing label. The user needs to be the creator of the + label to be able to do this. parameters: - description: Label ID in: path @@ -1676,11 +1778,13 @@ paths: - application/json description: Returns all lists a user has access to. parameters: - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -1859,7 +1963,9 @@ paths: - task /lists/{id}/background: delete: - description: Removes a previously set list background, regardless of the list provider used to set the background. It does not throw an error if the list does not have a background. + description: Removes a previously set list background, regardless of the list + provider used to set the background. It does not throw an error if the list + does not have a background. parameters: - description: List ID in: path @@ -2011,18 +2117,21 @@ paths: get: consumes: - application/json - description: Returns all kanban buckets with belong to a list including their tasks. + description: Returns all kanban buckets with belong to a list including their + tasks. parameters: - description: List Id in: path name: id required: true type: integer - - description: The page number for tasks. Used for pagination. If not provided, the first page of results is returned. + - description: The page number for tasks. Used for pagination. If not provided, + the first page of results is returned. in: query name: page type: integer - - description: The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page. + - description: The maximum number of tasks per bucket per page. This parameter + is limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -2030,7 +2139,10 @@ paths: in: query name: s type: string - - description: The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. + - description: The name of the field to filter by. Allowed values are all task + properties. Task properties which are their own object require passing in + the id of that entity. Accepts an array for multiple filters which will + be chanied together, all supplied filter must match. in: query name: filter_by type: string @@ -2038,15 +2150,20 @@ paths: in: query name: filter_value type: string - - description: The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals` + - description: The comparator to use for a filter. Available values are `equals`, + `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` + expects comma-separated values in `filter_value`. Defaults to `equals` in: query name: filter_comparator type: string - - description: The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`. + - description: The concatinator to use for filters. Available values are `and` + or `or`. Defaults to `or`. in: query name: filter_concat type: string - - description: If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`. + - description: If set to true the result will include filtered fields whose + value is set to `null`. Available values are `true` or `false`. Defaults + to `false`. in: query name: filter_include_nulls type: string @@ -2112,7 +2229,8 @@ paths: get: consumes: - application/json - description: Lists all users (without emailadresses). Also possible to search for a specific user. + description: Lists all users (without emailadresses). Also possible to search + for a specific user. parameters: - description: Search for a user by its name. in: query @@ -2160,11 +2278,13 @@ paths: name: id required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -2249,11 +2369,13 @@ paths: name: id required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -2338,11 +2460,13 @@ paths: name: list required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -2371,7 +2495,8 @@ paths: put: consumes: - application/json - description: Share a list via link. The user needs to have write-access to the list to be able do this. + description: Share a list via link. The user needs to have write-access to the + list to be able do this. parameters: - description: List ID in: path @@ -2416,7 +2541,8 @@ paths: delete: consumes: - application/json - description: Remove a link share. The user needs to have write-access to the list to be able do this. + description: Remove a link share. The user needs to have write-access to the + list to be able do this. parameters: - description: List ID in: path @@ -2495,7 +2621,8 @@ paths: delete: consumes: - application/json - description: Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. You cannot delete the last bucket on a list. + description: Deletes an existing kanban bucket and dissociates all of its task. + It does not delete any tasks. You cannot delete the last bucket on a list. parameters: - description: List Id in: path @@ -2576,7 +2703,10 @@ paths: put: consumes: - application/json - description: Copies the list, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one list to a new namespace. The user needs read access in the list and write access in the namespace of the new list. + description: Copies the list, tasks, files, kanban data, assignees, comments, + attachments, lables, relations, backgrounds, user/team rights and link shares + from one list to a new namespace. The user needs read access in the list and + write access in the namespace of the new list. parameters: - description: The list ID to duplicate in: path @@ -2624,11 +2754,13 @@ paths: name: listID required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -2636,15 +2768,24 @@ paths: in: query name: s type: string - - description: The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`. + - description: The sorting parameter. You can pass this multiple times to get + the tasks ordered by multiple different parametes, along with `order_by`. + Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, + `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, + `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default + is `id`. in: query name: sort_by type: string - - description: The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`. + - description: The ordering parameter. Possible values to order by are `asc` + or `desc`. Default is `asc`. in: query name: order_by type: string - - description: The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. + - description: The name of the field to filter by. Allowed values are all task + properties. Task properties which are their own object require passing in + the id of that entity. Accepts an array for multiple filters which will + be chanied together, all supplied filter must match. in: query name: filter_by type: string @@ -2652,15 +2793,20 @@ paths: in: query name: filter_value type: string - - description: The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals` + - description: The comparator to use for a filter. Available values are `equals`, + `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` + expects comma-separated values in `filter_value`. Defaults to `equals` in: query name: filter_comparator type: string - - description: The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`. + - description: The concatinator to use for filters. Available values are `and` + or `or`. Defaults to `or`. in: query name: filter_concat type: string - - description: If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`. + - description: If set to true the result will include filtered fields whose + value is set to `null`. Available values are `true` or `false`. Defaults + to `false`. in: query name: filter_include_nulls type: string @@ -2684,7 +2830,8 @@ paths: - task /lists/{listID}/teams/{teamID}: delete: - description: Delets a team from a list. The team won't have access to the list anymore. + description: Delets a team from a list. The team won't have access to the list + anymore. parameters: - description: List ID in: path @@ -2723,7 +2870,8 @@ paths: post: consumes: - application/json - description: Update a team <-> list relation. Mostly used to update the right that team has. + description: Update a team <-> list relation. Mostly used to update the right + that team has. parameters: - description: List ID in: path @@ -2767,7 +2915,8 @@ paths: - sharing /lists/{listID}/users/{userID}: delete: - description: Delets a user from a list. The user won't have access to the list anymore. + description: Delets a user from a list. The user won't have access to the list + anymore. parameters: - description: List ID in: path @@ -2806,7 +2955,8 @@ paths: post: consumes: - application/json - description: Update a user <-> list relation. Mostly used to update the right that user has. + description: Update a user <-> list relation. Mostly used to update the right + that user has. parameters: - description: List ID in: path @@ -2884,7 +3034,8 @@ paths: - user /migration/microsoft-todo/auth: get: - description: Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from Microsoft Todo to Vikunja. + description: Returns the auth url where the user needs to get its auth code. + This code can then be used to migrate everything from Microsoft Todo to Vikunja. produces: - application/json responses: @@ -2905,9 +3056,11 @@ paths: post: consumes: - application/json - description: Migrates all tasklinsts, tasks, notes and reminders from Microsoft Todo to Vikunja. + description: Migrates all tasklinsts, tasks, notes and reminders from Microsoft + Todo to Vikunja. parameters: - - description: The auth token previously obtained from the auth url. See the docs for /migration/microsoft-todo/auth. + - description: The auth token previously obtained from the auth url. See the + docs for /migration/microsoft-todo/auth. in: body name: migrationCode required: true @@ -2931,7 +3084,9 @@ paths: - migration /migration/microsoft-todo/status: get: - description: Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again. + description: Returns if the current user already did the migation or not. This + is useful to show a confirmation message in the frontend if the user is trying + to do the same migration again. produces: - application/json responses: @@ -2950,7 +3105,8 @@ paths: - migration /migration/todoist/auth: get: - description: Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from todoist to Vikunja. + description: Returns the auth url where the user needs to get its auth code. + This code can then be used to migrate everything from todoist to Vikunja. produces: - application/json responses: @@ -2971,9 +3127,11 @@ paths: post: consumes: - application/json - description: Migrates all projects, tasks, notes, reminders, subtasks and files from todoist to vikunja. + description: Migrates all projects, tasks, notes, reminders, subtasks and files + from todoist to vikunja. parameters: - - description: The auth code previously obtained from the auth url. See the docs for /migration/todoist/auth. + - description: The auth code previously obtained from the auth url. See the + docs for /migration/todoist/auth. in: body name: migrationCode required: true @@ -2997,7 +3155,9 @@ paths: - migration /migration/todoist/status: get: - description: Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again. + description: Returns if the current user already did the migation or not. This + is useful to show a confirmation message in the frontend if the user is trying + to do the same migration again. produces: - application/json responses: @@ -3016,7 +3176,8 @@ paths: - migration /migration/trello/auth: get: - description: Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from trello to Vikunja. + description: Returns the auth url where the user needs to get its auth code. + This code can then be used to migrate everything from trello to Vikunja. produces: - application/json responses: @@ -3037,9 +3198,11 @@ paths: post: consumes: - application/json - description: Migrates all projects, tasks, notes, reminders, subtasks and files from trello to vikunja. + description: Migrates all projects, tasks, notes, reminders, subtasks and files + from trello to vikunja. parameters: - - description: The auth token previously obtained from the auth url. See the docs for /migration/trello/auth. + - description: The auth token previously obtained from the auth url. See the + docs for /migration/trello/auth. in: body name: migrationCode required: true @@ -3063,7 +3226,9 @@ paths: - migration /migration/trello/status: get: - description: Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again. + description: Returns if the current user already did the migation or not. This + is useful to show a confirmation message in the frontend if the user is trying + to do the same migration again. produces: - application/json responses: @@ -3082,7 +3247,8 @@ paths: - migration /migration/wunderlist/auth: get: - description: Returns the auth url where the user needs to get its auth code. This code can then be used to migrate everything from wunderlist to Vikunja. + description: Returns the auth url where the user needs to get its auth code. + This code can then be used to migrate everything from wunderlist to Vikunja. produces: - application/json responses: @@ -3103,9 +3269,11 @@ paths: post: consumes: - application/json - description: Migrates all folders, lists, tasks, notes, reminders, subtasks and files from wunderlist to vikunja. + description: Migrates all folders, lists, tasks, notes, reminders, subtasks + and files from wunderlist to vikunja. parameters: - - description: The auth code previously obtained from the auth url. See the docs for /migration/wunderlist/auth. + - description: The auth code previously obtained from the auth url. See the + docs for /migration/wunderlist/auth. in: body name: migrationCode required: true @@ -3129,7 +3297,9 @@ paths: - migration /migration/wunderlist/status: get: - description: Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again. + description: Returns if the current user already did the migation or not. This + is useful to show a confirmation message in the frontend if the user is trying + to do the same migration again. produces: - application/json responses: @@ -3193,11 +3363,13 @@ paths: - application/json description: Returns all namespaces a user has access to. parameters: - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -3370,18 +3542,21 @@ paths: get: consumes: - application/json - description: Returns a namespace with all teams which have access on a given namespace. + description: Returns a namespace with all teams which have access on a given + namespace. parameters: - description: Namespace ID in: path name: id required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -3459,18 +3634,21 @@ paths: get: consumes: - application/json - description: Returns a namespace with all users which have access on a given namespace. + description: Returns a namespace with all users which have access on a given + namespace. parameters: - description: Namespace ID in: path name: id required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -3548,7 +3726,8 @@ paths: put: consumes: - application/json - description: Creates a new list in a given namespace. The user needs write-access to the namespace. + description: Creates a new list in a given namespace. The user needs write-access + to the namespace. parameters: - description: Namespace ID in: path @@ -3587,7 +3766,8 @@ paths: - list /namespaces/{namespaceID}/teams/{teamID}: delete: - description: Delets a team from a namespace. The team won't have access to the namespace anymore. + description: Delets a team from a namespace. The team won't have access to the + namespace anymore. parameters: - description: Namespace ID in: path @@ -3626,7 +3806,8 @@ paths: post: consumes: - application/json - description: Update a team <-> namespace relation. Mostly used to update the right that team has. + description: Update a team <-> namespace relation. Mostly used to update the + right that team has. parameters: - description: Namespace ID in: path @@ -3670,7 +3851,8 @@ paths: - sharing /namespaces/{namespaceID}/users/{userID}: delete: - description: Delets a user from a namespace. The user won't have access to the namespace anymore. + description: Delets a user from a namespace. The user won't have access to the + namespace anymore. parameters: - description: Namespace ID in: path @@ -3709,7 +3891,8 @@ paths: post: consumes: - application/json - description: Update a user <-> namespace relation. Mostly used to update the right that user has. + description: Update a user <-> namespace relation. Mostly used to update the + right that user has. parameters: - description: Namespace ID in: path @@ -3757,11 +3940,13 @@ paths: - application/json description: Returns an array with all notifications for the current user. parameters: - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -3791,7 +3976,8 @@ paths: post: consumes: - application/json - description: Marks a notification as either read or unread. A user can only mark their own notifications as read. + description: Marks a notification as either read or unread. A user can only + mark their own notifications as read. parameters: - description: Notification ID in: path @@ -3842,7 +4028,8 @@ paths: schema: $ref: '#/definitions/user.User' "400": - description: No or invalid user register object provided / User already exists. + description: No or invalid user register object provided / User already + exists. schema: $ref: '#/definitions/web.HTTPError' "500": @@ -3893,7 +4080,8 @@ paths: - application/json description: Unsubscribes the current user to an entity. parameters: - - description: The entity the user subscribed to. Can be either `namespace`, `list` or `task`. + - description: The entity the user subscribed to. Can be either `namespace`, + `list` or `task`. in: path name: entity required: true @@ -3932,7 +4120,8 @@ paths: - application/json description: Subscribes the current user to an entity. parameters: - - description: The entity the user subscribes to. Can be either `namespace`, `list` or `task`. + - description: The entity the user subscribes to. Can be either `namespace`, + `list` or `task`. in: path name: entity required: true @@ -4033,7 +4222,9 @@ paths: post: consumes: - application/json - description: Updates a task. This includes marking it as done. Assignees you pass will be updated, see their individual endpoints for more details on how this is done. To update labels, see the description of the endpoint. + description: Updates a task. This includes marking it as done. Assignees you + pass will be updated, see their individual endpoints for more details on how + this is done. To update labels, see the description of the endpoint. parameters: - description: Task ID in: path @@ -4081,11 +4272,13 @@ paths: name: id required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -4118,7 +4311,8 @@ paths: put: consumes: - multipart/form-data - description: Upload a task attachment. You can pass multiple files with the files form param. + description: Upload a task attachment. You can pass multiple files with the + files form param. parameters: - description: Task ID in: path @@ -4242,11 +4436,13 @@ paths: name: task required: true type: integer - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -4275,7 +4471,8 @@ paths: put: consumes: - application/json - description: Add a label to a task. The user needs to have write-access to the list to be able do this. + description: Add a label to a task. The user needs to have write-access to the + list to be able do this. parameters: - description: Task ID in: path @@ -4320,7 +4517,8 @@ paths: delete: consumes: - application/json - description: Remove a label from a task. The user needs to have write-access to the list to be able do this. + description: Remove a label from a task. The user needs to have write-access + to the list to be able do this. parameters: - description: Task ID in: path @@ -4362,11 +4560,13 @@ paths: - application/json description: Returns an array with all assignees for this task. parameters: - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -4400,7 +4600,8 @@ paths: put: consumes: - application/json - description: Adds a new assignee to a task. The assignee needs to have access to the list, the doer must be able to edit this task. + description: Adds a new assignee to a task. The assignee needs to have access + to the list, the doer must be able to edit this task. parameters: - description: The assingee object in: body @@ -4473,7 +4674,10 @@ paths: post: consumes: - application/json - description: Adds multiple new assignees to a task. The assignee needs to have access to the list, the doer must be able to edit this task. Every user not in the list will be unassigned from the task, pass an empty array to unassign everyone. + description: Adds multiple new assignees to a task. The assignee needs to have + access to the list, the doer must be able to edit this task. Every user not + in the list will be unassigned from the task, pass an empty array to unassign + everyone. parameters: - description: The array of assignees in: body @@ -4510,7 +4714,8 @@ paths: get: consumes: - application/json - description: Get all task comments. The user doing this need to have at least read access to the task. + description: Get all task comments. The user doing this need to have at least + read access to the task. parameters: - description: Task ID in: path @@ -4538,7 +4743,8 @@ paths: put: consumes: - application/json - description: Create a new task comment. The user doing this need to have at least write access to the task this comment should belong to. + description: Create a new task comment. The user doing this need to have at + least write access to the task this comment should belong to. parameters: - description: The task comment object in: body @@ -4575,7 +4781,8 @@ paths: delete: consumes: - application/json - description: Remove a task comment. The user doing this need to have at least write access to the task this comment belongs to. + description: Remove a task comment. The user doing this need to have at least + write access to the task this comment belongs to. parameters: - description: Task ID in: path @@ -4614,7 +4821,8 @@ paths: get: consumes: - application/json - description: Remove a task comment. The user doing this need to have at least read access to the task this comment belongs to. + description: Remove a task comment. The user doing this need to have at least + read access to the task this comment belongs to. parameters: - description: Task ID in: path @@ -4653,7 +4861,8 @@ paths: post: consumes: - application/json - description: Update an existing task comment. The user doing this need to have at least write access to the task this comment belongs to. + description: Update an existing task comment. The user doing this need to have + at least write access to the task this comment belongs to. parameters: - description: Task ID in: path @@ -4693,7 +4902,10 @@ paths: post: consumes: - application/json - description: Updates all labels on a task. Every label which is not passed but exists on the task will be deleted. Every label which does not exist on the task will be added. All labels which are passed and already exist on the task won't be touched. + description: Updates all labels on a task. Every label which is not passed but + exists on the task will be deleted. Every label which does not exist on the + task will be added. All labels which are passed and already exist on the task + won't be touched. parameters: - description: The array of labels in: body @@ -4730,7 +4942,10 @@ paths: put: consumes: - application/json - description: Creates a new relation between two tasks. The user needs to have update rights on the base task and at least read rights on the other task. Both tasks do not need to be on the same list. Take a look at the docs for available task relation kinds. + description: Creates a new relation between two tasks. The user needs to have + update rights on the base task and at least read rights on the other task. + Both tasks do not need to be on the same list. Take a look at the docs for + available task relation kinds. parameters: - description: The relation object in: body @@ -4779,7 +4994,8 @@ paths: name: taskID required: true type: integer - - description: The kind of the relation. See the TaskRelation type for more info. + - description: The kind of the relation. See the TaskRelation type for more + info. in: path name: relationKind required: true @@ -4819,11 +5035,13 @@ paths: - application/json description: Returns all tasks on any list the user has access to. parameters: - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -4831,15 +5049,24 @@ paths: in: query name: s type: string - - description: The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`. + - description: The sorting parameter. You can pass this multiple times to get + the tasks ordered by multiple different parametes, along with `order_by`. + Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, + `due_date`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date`, + `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default + is `id`. in: query name: sort_by type: string - - description: The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`. + - description: The ordering parameter. Possible values to order by are `asc` + or `desc`. Default is `asc`. in: query name: order_by type: string - - description: The name of the field to filter by. Allowed values are all task properties. Task properties which are their own object require passing in the id of that entity. Accepts an array for multiple filters which will be chanied together, all supplied filter must match. + - description: The name of the field to filter by. Allowed values are all task + properties. Task properties which are their own object require passing in + the id of that entity. Accepts an array for multiple filters which will + be chanied together, all supplied filter must match. in: query name: filter_by type: string @@ -4847,15 +5074,20 @@ paths: in: query name: filter_value type: string - - description: The comparator to use for a filter. Available values are `equals`, `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` expects comma-separated values in `filter_value`. Defaults to `equals` + - description: The comparator to use for a filter. Available values are `equals`, + `greater`, `greater_equals`, `less`, `less_equals`, `like` and `in`. `in` + expects comma-separated values in `filter_value`. Defaults to `equals` in: query name: filter_comparator type: string - - description: The concatinator to use for filters. Available values are `and` or `or`. Defaults to `or`. + - description: The concatinator to use for filters. Available values are `and` + or `or`. Defaults to `or`. in: query name: filter_concat type: string - - description: If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`. + - description: If set to true the result will include filtered fields whose + value is set to `null`. Available values are `true` or `false`. Defaults + to `false`. in: query name: filter_include_nulls type: string @@ -4881,9 +5113,12 @@ paths: post: consumes: - application/json - description: 'Updates a bunch of tasks at once. This includes marking them as done. Note: although you could supply another ID, it will be ignored. Use task_ids instead.' + description: 'Updates a bunch of tasks at once. This includes marking them as + done. Note: although you could supply another ID, it will be ignored. Use + task_ids instead.' parameters: - - description: The task object. Looks like a normal task, the only difference is it uses an array of list_ids to update. + - description: The task object. Looks like a normal task, the only difference + is it uses an array of list_ids to update. in: body name: task required: true @@ -4919,11 +5154,13 @@ paths: - application/json description: Returns all teams the current user is part of. parameters: - - description: The page number. Used for pagination. If not provided, the first page of results is returned. + - description: The page number. Used for pagination. If not provided, the first + page of results is returned. in: query name: page type: integer - - description: The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page. + - description: The maximum number of items per page. Note this parameter is + limited by the configured maximum of items per page. in: query name: per_page type: integer @@ -4952,7 +5189,8 @@ paths: put: consumes: - application/json - description: Creates a new team in a given namespace. The user needs write-access to the namespace. + description: Creates a new team in a given namespace. The user needs write-access + to the namespace. parameters: - description: The team you want to create. in: body @@ -4982,7 +5220,8 @@ paths: - team /teams/{id}: delete: - description: Delets a team. This will also remove the access for all users in that team. + description: Delets a team. This will also remove the access for all users in + that team. parameters: - description: Team ID in: path @@ -5118,7 +5357,8 @@ paths: - team /teams/{id}/members/{userID}: delete: - description: Remove a user from a team. This will also revoke any access this user might have via that team. + description: Remove a user from a team. This will also revoke any access this + user might have via that team. parameters: - description: Team ID in: path @@ -5180,7 +5420,10 @@ paths: patch: consumes: - application/json - description: 'Fills the specified table with the content provided in the payload. You need to enable the testing endpoint before doing this and provide the `Authorization: ` secret when making requests to this endpoint. See docs for more details.' + description: 'Fills the specified table with the content provided in the payload. + You need to enable the testing endpoint before doing this and provide the + `Authorization: ` secret when making requests to this endpoint. See + docs for more details.' parameters: - description: The table to reset in: path @@ -5328,7 +5571,8 @@ paths: post: consumes: - application/json - description: Requests a token to reset a users password. The token is sent via email. + description: Requests a token to reset a users password. The token is sent via + email. parameters: - description: The username of the user to request a token for. in: body @@ -5382,7 +5626,8 @@ paths: post: consumes: - application/json - description: Changes the user avatar. Valid types are gravatar (uses the user email), upload, initials, default. + description: Changes the user avatar. Valid types are gravatar (uses the user + email), upload, initials, default. parameters: - description: The user's avatar setting in: body @@ -5414,7 +5659,8 @@ paths: put: consumes: - multipart/form-data - description: Upload a user avatar. This will also set the user's avatar provider to "upload" + description: Upload a user avatar. This will also set the user's avatar provider + to "upload" parameters: - description: The avatar as single file. in: formData @@ -5516,7 +5762,8 @@ paths: get: consumes: - application/json - description: Returns the current user totp setting or an error if it is not enabled. + description: Returns the current user totp setting or an error if it is not + enabled. produces: - application/json responses: @@ -5573,7 +5820,8 @@ paths: post: consumes: - application/json - description: Enables a previously enrolled totp setting by providing a totp passcode. + description: Enables a previously enrolled totp setting by providing a totp + passcode. parameters: - description: The totp passcode. in: body @@ -5613,7 +5861,9 @@ paths: post: consumes: - application/json - description: Creates an initial setup for the user in the db. After this step, the user needs to verify they have a working totp setup with the "enable totp" endpoint. + description: Creates an initial setup for the user in the db. After this step, + the user needs to verify they have a working totp setup with the "enable totp" + endpoint. produces: - application/json responses: @@ -5682,7 +5932,8 @@ paths: get: consumes: - application/json - description: Search for a user by its username, name or full email. Name (not username) or email require that the user has enabled this in their settings. + description: Search for a user by its username, name or full email. Name (not + username) or email require that the user has enabled this in their settings. parameters: - description: The search criteria. in: query diff --git a/pkg/user/user.go b/pkg/user/user.go index 527cf352..8b32510a 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -67,11 +67,10 @@ type User struct { Issuer string `xorm:"text null" json:"-"` Subject string `xorm:"text null" json:"-"` - // If enabled, sends email reminders of tasks to the user. - EmailRemindersEnabled bool `xorm:"bool default true" json:"-"` - - DiscoverableByName bool `xorm:"bool default false index" json:"-"` - DiscoverableByEmail bool `xorm:"bool default false index" json:"-"` + EmailRemindersEnabled bool `xorm:"bool default true" json:"-"` + DiscoverableByName bool `xorm:"bool default false index" json:"-"` + DiscoverableByEmail bool `xorm:"bool default false index" json:"-"` + OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"` // A timestamp when this task was created. You cannot change this value. Created time.Time `xorm:"created not null" json:"created"` @@ -371,6 +370,7 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) { "email_reminders_enabled", "discoverable_by_name", "discoverable_by_email", + "overdue_tasks_reminders_enabled", ). Update(user) if err != nil { diff --git a/pkg/utils/time.go b/pkg/utils/time.go new file mode 100644 index 00000000..dbd2f3cb --- /dev/null +++ b/pkg/utils/time.go @@ -0,0 +1,32 @@ +// 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 . + +package utils + +import ( + "time" + + "code.vikunja.io/api/pkg/config" +) + +// GetTimeWithoutNanoSeconds returns a time.Time without the nanoseconds. +func GetTimeWithoutNanoSeconds(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(), t.Second(), 0, t.Location()).In(tz) +}