Add email reminders #743
|
@ -39,6 +39,9 @@ service:
|
|||
# each request made to this endpoint neefs to provide an `Authorization: <token>` header with the token from below. <br/>
|
||||
# **You should never use this unless you know exactly what you're doing**
|
||||
testingtoken: ''
|
||||
# If enabled, vikunja will send an email to everyone who is either assigned to a task or created it when a task reminder
|
||||
# is due.
|
||||
enableemailreminders: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
|
|
|
@ -170,6 +170,13 @@ each request made to this endpoint neefs to provide an `Authorization: <token>`
|
|||
|
||||
Default: `<empty>`
|
||||
|
||||
### enableemailreminders
|
||||
|
||||
If enabled, vikunja will send an email to everyone who is either assigned to a task or created it when a task reminder
|
||||
is due.
|
||||
|
||||
Default: `true`
|
||||
|
||||
---
|
||||
|
||||
## database
|
||||
|
|
1
go.mod
1
go.mod
|
@ -61,6 +61,7 @@ require (
|
|||
github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect
|
||||
github.com/pquerna/otp v1.3.0
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
|
||||
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546
|
||||
|
|
2
go.sum
2
go.sum
|
@ -679,6 +679,8 @@ github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULU
|
|||
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
|
|
|
@ -22,6 +22,8 @@ import (
|
|||
"os/signal"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/initialize"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
@ -70,5 +72,6 @@ var webCmd = &cobra.Command{
|
|||
if err := e.Shutdown(ctx); err != nil {
|
||||
e.Logger.Fatal(err)
|
||||
}
|
||||
cron.Stop()
|
||||
},
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ const (
|
|||
ServiceEnableTotp Key = `service.enabletotp`
|
||||
ServiceSentryDsn Key = `service.sentrydsn`
|
||||
ServiceTestingtoken Key = `service.testingtoken`
|
||||
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
||||
|
||||
AuthLocalEnabled Key = `auth.local.enabled`
|
||||
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
||||
|
@ -233,6 +234,7 @@ func InitDefaultConfig() {
|
|||
ServiceTimeZone.setDefault("GMT")
|
||||
ServiceEnableTaskComments.setDefault(true)
|
||||
ServiceEnableTotp.setDefault(true)
|
||||
ServiceEnableEmailReminders.setDefault(true)
|
||||
|
||||
// Auth
|
||||
AuthLocalEnabled.setDefault(true)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cron
|
||||
|
||||
import (
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var c *cron.Cron
|
||||
|
||||
// Init starts the cron
|
||||
func Init() {
|
||||
c = cron.New()
|
||||
c.Start()
|
||||
}
|
||||
|
||||
// Schedule schedules a job as a cron job
|
||||
func Schedule(schedule string, f func()) (err error) {
|
||||
_, err = c.AddFunc(schedule, f)
|
||||
return
|
||||
}
|
||||
|
||||
// Stop stops the cron scheduler
|
||||
func Stop() {
|
||||
c.Stop()
|
||||
}
|
|
@ -18,6 +18,7 @@ package initialize
|
|||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
|
@ -80,4 +81,8 @@ func FullInit() {
|
|||
|
||||
// Start the mail daemon
|
||||
mail.StartMailDaemon()
|
||||
|
||||
// Start the cron
|
||||
cron.Init()
|
||||
models.RegisterReminderCron()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type users20201218220204 struct {
|
||||
EmailRemindersEnabled bool `xorm:"bool default true" json:"-"`
|
||||
}
|
||||
|
||||
func (users20201218220204) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20201218220204",
|
||||
Description: "Add email reminder setting to user",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(users20201218220204{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
|
@ -53,12 +53,13 @@ func TestLabelTask_ReadAll(t *testing.T) {
|
|||
Updated: testUpdatedTime,
|
||||
CreatedByID: 2,
|
||||
CreatedBy: &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -48,13 +48,14 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
page int
|
||||
}
|
||||
user1 := &user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -97,12 +98,13 @@ func TestLabel_ReadAll(t *testing.T) {
|
|||
Updated: testUpdatedTime,
|
||||
CreatedByID: 2,
|
||||
CreatedBy: &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -157,13 +159,14 @@ 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",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
|
@ -217,12 +220,13 @@ 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",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
|
|
|
@ -172,24 +172,26 @@ func TestListUser_ReadAll(t *testing.T) {
|
|||
want: []*UserWithRight{
|
||||
{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
|
|
|
@ -171,24 +171,26 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
|
|||
want: []*UserWithRight{
|
||||
{
|
||||
User: user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
{
|
||||
User: user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
},
|
||||
Right: RightRead,
|
||||
},
|
||||
|
|
|
@ -31,30 +31,33 @@ 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",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
user2 := &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
user6 := &user.User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
IsActive: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
IsActive: true,
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
||||
loc := config.GetTimeZone()
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-2020 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 General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/mail"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
)
|
||||
|
||||
// TaskReminder holds a reminder on a task
|
||||
type TaskReminder struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk"`
|
||||
TaskID int64 `xorm:"bigint not null INDEX"`
|
||||
Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'"`
|
||||
Created time.Time `xorm:"created not null"`
|
||||
}
|
||||
|
||||
// TableName returns a pretty table name
|
||||
func (TaskReminder) TableName() string {
|
||||
return "task_reminders"
|
||||
}
|
||||
|
||||
type taskUser struct {
|
||||
Task *Task `xorm:"extends"`
|
||||
User *user.User `xorm:"extends"`
|
||||
}
|
||||
|
||||
func getTaskUsersForTasks(taskIDs []int64) (taskUsers []*taskUser, err error) {
|
||||
// Get all creators of tasks
|
||||
creators := make(map[int64]*user.User, len(taskIDs))
|
||||
err = x.
|
||||
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").
|
||||
GroupBy("tasks.id, users.id, users.username, users.email, users.name").
|
||||
Find(&creators)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
assignees, err := getRawTaskAssigneesForTasks(taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
taskMap := make(map[int64]*Task, len(taskIDs))
|
||||
err = x.In("id", taskIDs).Find(&taskMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, taskID := range taskIDs {
|
||||
taskUsers = append(taskUsers, &taskUser{
|
||||
Task: taskMap[taskID],
|
||||
User: creators[taskMap[taskID].CreatedByID],
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RegisterReminderCron registers a cron function which runs every minute to check if any reminders are due the
|
||||
// next minute to send emails.
|
||||
func RegisterReminderCron() {
|
||||
if !config.ServiceEnableEmailReminders.GetBool() {
|
||||
return
|
||||
}
|
||||
|
||||
if !config.MailerEnabled.GetBool() {
|
||||
log.Info("Mailer is disabled, not sending reminders per mail")
|
||||
return
|
||||
}
|
||||
|
||||
tz := config.GetTimeZone()
|
||||
const dbFormat = `2006-01-02 15:04:05`
|
||||
|
||||
log.Debugf("[Task Reminder Cron] Timezone is %s", tz)
|
||||
|
||||
err := cron.Schedule("* * * * *", func() {
|
||||
// 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.Now()
|
||||
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)
|
||||
|
||||
reminders := []*TaskReminder{}
|
||||
err := x.
|
||||
Where("reminder >= ? and reminder < ?", now.Format(dbFormat), nextMinute.Format(dbFormat)).
|
||||
Find(&reminders)
|
||||
if err != nil {
|
||||
log.Errorf("[Task Reminder Cron] Could not get reminders for the next minute: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Task Reminder Cron] Found %d reminders", len(reminders))
|
||||
|
||||
if len(reminders) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// We're sending a reminder to everyone who is assigned to the task or has created it.
|
||||
var taskIDs []int64
|
||||
for _, r := range reminders {
|
||||
taskIDs = append(taskIDs, r.TaskID)
|
||||
}
|
||||
|
||||
users, err := getTaskUsersForTasks(taskIDs)
|
||||
if err != nil {
|
||||
log.Errorf("[Task Reminder Cron] Could not get task users to send them reminders: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("[Task Reminder Cron] Sending reminders to %d users", len(users))
|
||||
|
||||
for _, u := range users {
|
||||
data := map[string]interface{}{
|
||||
"User": u.User,
|
||||
"Task": u.Task,
|
||||
}
|
||||
|
||||
mail.SendMailWithTemplate(u.User.Email, `Reminder for "`+u.Task.Title+`"`, "reminder-email", data)
|
||||
log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not register reminder cron: %s", err)
|
||||
}
|
||||
}
|
|
@ -116,19 +116,6 @@ func (Task) TableName() string {
|
|||
return "tasks"
|
||||
}
|
||||
|
||||
// TaskReminder holds a reminder on a task
|
||||
type TaskReminder struct {
|
||||
ID int64 `xorm:"bigint autoincr not null unique pk"`
|
||||
TaskID int64 `xorm:"bigint not null INDEX"`
|
||||
Reminder time.Time `xorm:"DATETIME not null INDEX 'reminder'"`
|
||||
Created time.Time `xorm:"created not null"`
|
||||
}
|
||||
|
||||
// TableName returns a pretty table name
|
||||
func (TaskReminder) TableName() string {
|
||||
return "task_reminders"
|
||||
}
|
||||
|
||||
type taskFilterConcatinator string
|
||||
|
||||
const (
|
||||
|
|
|
@ -27,122 +27,135 @@ import (
|
|||
|
||||
func TestListUsersFromList(t *testing.T) {
|
||||
testuser1 := &user.User{
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 1,
|
||||
Username: "user1",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser2 := &user.User{
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 2,
|
||||
Username: "user2",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser3 := &user.User{
|
||||
ID: 3,
|
||||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
PasswordResetToken: "passwordresettesttoken",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 3,
|
||||
Username: "user3",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
PasswordResetToken: "passwordresettesttoken",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser4 := &user.User{
|
||||
ID: 4,
|
||||
Username: "user4",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 4,
|
||||
Username: "user4",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser5 := &user.User{
|
||||
ID: 5,
|
||||
Username: "user5",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 5,
|
||||
Username: "user5",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: false,
|
||||
EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser6 := &user.User{
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 6,
|
||||
Username: "user6",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser7 := &user.User{
|
||||
ID: 7,
|
||||
Username: "user7",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 7,
|
||||
Username: "user7",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser8 := &user.User{
|
||||
ID: 8,
|
||||
Username: "user8",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 8,
|
||||
Username: "user8",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser9 := &user.User{
|
||||
ID: 9,
|
||||
Username: "user9",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 9,
|
||||
Username: "user9",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser10 := &user.User{
|
||||
ID: 10,
|
||||
Username: "user10",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 10,
|
||||
Username: "user10",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser11 := &user.User{
|
||||
ID: 11,
|
||||
Username: "user11",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 11,
|
||||
Username: "user11",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser12 := &user.User{
|
||||
ID: 12,
|
||||
Username: "user12",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 12,
|
||||
Username: "user12",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
testuser13 := &user.User{
|
||||
ID: 13,
|
||||
Username: "user13",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
ID: 13,
|
||||
Username: "user13",
|
||||
Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
|
||||
IsActive: true,
|
||||
Issuer: "local",
|
||||
EmailRemindersEnabled: true,
|
||||
Created: testCreatedTime,
|
||||
Updated: testUpdatedTime,
|
||||
}
|
||||
|
||||
type args struct {
|
||||
|
|
|
@ -62,6 +62,7 @@ func NewUserJWTAuthtoken(user *user.User) (token string, err error) {
|
|||
claims["email"] = user.Email
|
||||
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
||||
claims["name"] = user.Name
|
||||
claims["emailRemindersEnabled"] = user.EmailRemindersEnabled
|
||||
|
||||
// Generate encoded token and send it as response.
|
||||
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
|
||||
|
|
|
@ -47,6 +47,7 @@ type vikunjaInfos struct {
|
|||
Legal legalInfo `json:"legal"`
|
||||
CaldavEnabled bool `json:"caldav_enabled"`
|
||||
AuthInfo authInfo `json:"auth"`
|
||||
EmailRemindersEnabled bool `json:"email_reminders_enabled"`
|
||||
}
|
||||
|
||||
type authInfo struct {
|
||||
|
@ -87,6 +88,7 @@ func Info(c echo.Context) error {
|
|||
TaskAttachmentsEnabled: config.ServiceEnableTaskAttachments.GetBool(),
|
||||
TotpEnabled: config.ServiceEnableTotp.GetBool(),
|
||||
CaldavEnabled: config.ServiceEnableCaldav.GetBool(),
|
||||
EmailRemindersEnabled: config.ServiceEnableEmailReminders.GetBool(),
|
||||
Legal: legalInfo{
|
||||
ImprintURL: config.LegalImprintURL.GetString(),
|
||||
PrivacyPolicyURL: config.LegalPrivacyURL.GetString(),
|
||||
|
|
|
@ -31,10 +31,12 @@ type UserAvatarProvider struct {
|
|||
AvatarProvider string `json:"avatar_provider"`
|
||||
}
|
||||
|
||||
// UserName holds the user's name
|
||||
type UserName struct {
|
||||
// UserSettings holds all user settings
|
||||
type UserSettings struct {
|
||||
// The new name of the current user.
|
||||
Name string `json:"name"`
|
||||
// If enabled, sends email reminders of tasks to the user.
|
||||
EmailRemindersEnabled bool `xorm:"bool default false" json:"email_reminders_enabled"`
|
||||
}
|
||||
|
||||
// GetUserAvatarProvider returns the currently set user avatar
|
||||
|
@ -104,21 +106,20 @@ func ChangeUserAvatarProvider(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, &models.Message{Message: "Avatar was changed successfully."})
|
||||
}
|
||||
|
||||
// ChangeUserName is the handler to change the name of the current user
|
||||
// @Summary Change the current user's name
|
||||
// @Description Changes the current user's name. It is also possible to reset the name.
|
||||
// UpdateGeneralUserSettings is the handler to change general user settings
|
||||
// @Summary Change general user settings of the current user.
|
||||
// @tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security JWTKeyAuth
|
||||
// @Param avatar body UserName true "The updated user name"
|
||||
// @Param avatar body UserSettings true "The updated user settings"
|
||||
// @Success 200 {object} models.Message
|
||||
// @Failure 400 {object} web.HTTPError "Something's invalid."
|
||||
// @Failure 500 {object} models.Message "Internal server error."
|
||||
// @Router /user/settings/name [post]
|
||||
func UpdateUserName(c echo.Context) error {
|
||||
un := &UserName{}
|
||||
err := c.Bind(un)
|
||||
// @Router /user/settings/general [post]
|
||||
func UpdateGeneralUserSettings(c echo.Context) error {
|
||||
us := &UserSettings{}
|
||||
err := c.Bind(us)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Bad user name provided.")
|
||||
}
|
||||
|
@ -133,12 +134,13 @@ func UpdateUserName(c echo.Context) error {
|
|||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
user.Name = un.Name
|
||||
user.Name = us.Name
|
||||
user.EmailRemindersEnabled = us.EmailRemindersEnabled
|
||||
|
||||
_, err = user2.UpdateUser(user)
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, &models.Message{Message: "Name was changed successfully."})
|
||||
return c.JSON(http.StatusOK, &models.Message{Message: "The settings were updated successfully."})
|
||||
}
|
||||
|
|
|
@ -278,7 +278,7 @@ func registerAPIRoutes(a *echo.Group) {
|
|||
u.GET("/settings/avatar", apiv1.GetUserAvatarProvider)
|
||||
u.POST("/settings/avatar", apiv1.ChangeUserAvatarProvider)
|
||||
u.PUT("/settings/avatar/upload", apiv1.UploadAvatar)
|
||||
u.POST("/settings/name", apiv1.UpdateUserName)
|
||||
u.POST("/settings/general", apiv1.UpdateGeneralUserSettings)
|
||||
|
||||
if config.ServiceEnableTotp.GetBool() {
|
||||
u.GET("/settings/totp", apiv1.UserTOTP)
|
||||
|
|
|
@ -6296,14 +6296,13 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/name": {
|
||||
"/user/settings/general": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Changes the current user's name. It is also possible to reset the name.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -6313,15 +6312,15 @@ var doc = `{
|
|||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Change the current user's name",
|
||||
"summary": "Change general user settings of the current user.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The updated user name",
|
||||
"description": "The updated user settings",
|
||||
"name": "avatar",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserName"
|
||||
"$ref": "#/definitions/v1.UserSettings"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -8008,15 +8007,6 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.UserName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The new name of the current user.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserPassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8028,6 +8018,19 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.UserSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email_reminders_enabled": {
|
||||
"description": "If enabled, sends email reminders of tasks to the user.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "The new name of the current user.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.authInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8090,6 +8093,9 @@ var doc = `{
|
|||
"caldav_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"email_reminders_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled_background_providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -6279,14 +6279,13 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/user/settings/name": {
|
||||
"/user/settings/general": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Changes the current user's name. It is also possible to reset the name.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -6296,15 +6295,15 @@
|
|||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"summary": "Change the current user's name",
|
||||
"summary": "Change general user settings of the current user.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The updated user name",
|
||||
"description": "The updated user settings",
|
||||
"name": "avatar",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.UserName"
|
||||
"$ref": "#/definitions/v1.UserSettings"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -7991,15 +7990,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.UserName": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The new name of the current user.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.UserPassword": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8011,6 +8001,19 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1.UserSettings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email_reminders_enabled": {
|
||||
"description": "If enabled, sends email reminders of tasks to the user.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"description": "The new name of the current user.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.authInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8073,6 +8076,9 @@
|
|||
"caldav_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"email_reminders_enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled_background_providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -947,12 +947,6 @@ definitions:
|
|||
description: The avatar provider. Valid types are `gravatar` (uses the user email), `upload`, `initials`, `default`.
|
||||
type: string
|
||||
type: object
|
||||
v1.UserName:
|
||||
properties:
|
||||
name:
|
||||
description: The new name of the current user.
|
||||
type: string
|
||||
type: object
|
||||
v1.UserPassword:
|
||||
properties:
|
||||
new_password:
|
||||
|
@ -960,6 +954,15 @@ definitions:
|
|||
old_password:
|
||||
type: string
|
||||
type: object
|
||||
v1.UserSettings:
|
||||
properties:
|
||||
email_reminders_enabled:
|
||||
description: If enabled, sends email reminders of tasks to the user.
|
||||
type: boolean
|
||||
name:
|
||||
description: The new name of the current user.
|
||||
type: string
|
||||
type: object
|
||||
v1.authInfo:
|
||||
properties:
|
||||
local:
|
||||
|
@ -1000,6 +1003,8 @@ definitions:
|
|||
type: array
|
||||
caldav_enabled:
|
||||
type: boolean
|
||||
email_reminders_enabled:
|
||||
type: boolean
|
||||
enabled_background_providers:
|
||||
items:
|
||||
type: string
|
||||
|
@ -5100,18 +5105,17 @@ paths:
|
|||
summary: Update email address
|
||||
tags:
|
||||
- user
|
||||
/user/settings/name:
|
||||
/user/settings/general:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Changes the current user's name. It is also possible to reset the name.
|
||||
parameters:
|
||||
- description: The updated user name
|
||||
- description: The updated user settings
|
||||
in: body
|
||||
name: avatar
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/v1.UserName'
|
||||
$ref: '#/definitions/v1.UserSettings'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -5129,7 +5133,7 @@ paths:
|
|||
$ref: '#/definitions/models.Message'
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Change the current user's name
|
||||
summary: Change general user settings of the current user.
|
||||
tags:
|
||||
- user
|
||||
/user/settings/totp:
|
||||
|
|
|
@ -62,6 +62,9 @@ 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:"-"`
|
||||
|
||||
// A timestamp when this task was created. You cannot change this value.
|
||||
Created time.Time `xorm:"created not null" json:"created"`
|
||||
// A timestamp when this task was last updated. You cannot change this value.
|
||||
|
@ -322,6 +325,7 @@ func UpdateUser(user *User) (updatedUser *User, err error) {
|
|||
"avatar_file_id",
|
||||
"is_active",
|
||||
"name",
|
||||
"email_reminders_enabled",
|
||||
).
|
||||
Update(user)
|
||||
if err != nil {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<br/>
|
||||
To confirm your email address, click the link below:
|
||||
</p>
|
||||
<a href="{{.FrontendURL}}?userEmailConfirm={{.User.EmailConfirmToken}}" title="Confirm your email address" style="background: rgb(20, 131, 175); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; border: 1px solid rgb(16, 106, 140); border-bottom-width: 3px; color: rgb(255, 255, 255); font-weight: 700; font-size: 13px; margin: 10px auto; padding: 5px 10px; text-decoration: none; text-align: center; text-rendering: optimizelegibility; text-transform: uppercase; display: block; width: 200px;">
|
||||
<a href="{{.FrontendURL}}?userEmailConfirm={{.User.EmailConfirmToken}}" title="Confirm your email address" style="-webkit-box-shadow: 0.3em 0.3em 1em #b2d0ff; box-shadow: 0.3em 0.3em 1em #b2d0ff; background-color: #1973ff; border-color: transparent; color: #fff; text-decoration: none; text-align: center; text-rendering: optimizelegibility; text-transform: uppercase; font-weight: bold; font-size: 14px; padding: 10px 14px; margin: 10px auto; border-radius: 4px; user-select: none; display: block; width: 280px;font-family:sans-serif">
|
||||
Confirm your email address
|
||||
</a>
|
||||
<p>
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<head>
|
||||
<meta name="viewport" content="width: display-width;">
|
||||
</head>
|
||||
<body style="width: 100%; padding: 0; margin: 0; background: #fbfbfb">
|
||||
<body style="width: 100%; padding: 0; margin: 0; background: #f1f5f8">
|
||||
<div style="width: 100%; font-family: 'Open Sans', sans-serif; text-rendering: optimizeLegibility">
|
||||
<div style="width: 600px; margin: 0 auto; text-align: justify;">
|
||||
<h1 style="font-size: 30px; text-align: center;">
|
||||
<img src="{{.FrontendURL}}images/logo-full.svg" style="height: 75px;" alt="Vikunja"/>
|
||||
</h1>
|
||||
<div style="border: 1px solid #ccc; box-shadow: 1px 1px 5px #eeeeee; padding: 5px 25px; border-radius: 3px; background: #fff;">
|
||||
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
|
|
@ -0,0 +1,17 @@
|
|||
{{template "mail-header.tmpl" .}}
|
||||
<p>
|
||||
Hi {{if .User.Name}}{{.User.Name}}{{else}}{{.User.Username}}{{end}},<br/>
|
||||
<br/>
|
||||
This is a friendly reminder of the task "{{.Task.Title}}".<br/>
|
||||
</p>
|
||||
<a href="{{.FrontendURL}}tasks/{{.Task.ID}}" title="Open Task" style="-webkit-box-shadow: 0.3em 0.3em 1em #b2d0ff; box-shadow: 0.3em 0.3em 1em #b2d0ff; background-color: #1973ff; border-color: transparent; color: #fff; text-decoration: none; text-align: center; text-rendering: optimizelegibility; text-transform: uppercase; font-weight: bold; font-size: 14px; padding: 10px 14px; margin: 10px auto; border-radius: 4px; user-select: none; display: block; width: 280px;font-family:sans-serif">
|
||||
Open Task
|
||||
</a>
|
||||
<p>
|
||||
If the button above doesn't work, copy the url below and paste it in your browsers address bar:<br/>
|
||||
{{.FrontendURL}}tasks/{{.Task.ID}}
|
||||
</p>
|
||||
<p>
|
||||
Have a nice day!
|
||||
</p>
|
||||
{{template "mail-footer.tmpl"}}
|
|
@ -0,0 +1,9 @@
|
|||
Hi {{if .User.Name}}{{.User.Name}}{{else}}{{.User.Username}}{{end}},
|
||||
|
||||
This is a friendly reminder of the task "{{.Task.Title}}".
|
||||
|
||||
You can view the task at:
|
||||
|
||||
{{.FrontendURL}}tasks/{{.Task.ID}}
|
||||
|
||||
Have a nice day!
|
|
@ -4,7 +4,7 @@
|
|||
<br>
|
||||
To reset your password, click the link below:
|
||||
</p>
|
||||
<a href="{{.FrontendURL}}?userPasswordReset={{.User.PasswordResetToken}}" title="Reset your password" style="background: rgb(20, 131, 175); -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; border: 1px solid rgb(16, 106, 140); border-bottom-width: 3px; color: rgb(255, 255, 255); font-weight: 700; font-size: 13px; margin: 10px auto; padding: 5px 10px; text-decoration: none; text-align: center; text-rendering: optimizelegibility; text-transform: uppercase; display: block; width: 200px;">
|
||||
<a href="{{.FrontendURL}}?userPasswordReset={{.User.PasswordResetToken}}" title="Reset your password" style="-webkit-box-shadow: 0.3em 0.3em 1em #b2d0ff; box-shadow: 0.3em 0.3em 1em #b2d0ff; background-color: #1973ff; border-color: transparent; color: #fff; text-decoration: none; text-align: center; text-rendering: optimizelegibility; text-transform: uppercase; font-weight: bold; font-size: 14px; padding: 10px 14px; margin: 10px auto; border-radius: 4px; user-select: none; display: block; width: 280px;font-family:sans-serif">
|
||||
Reset your password
|
||||
</a>
|
||||
<p>
|
||||
|
|
Loading…
Reference in New Issue