Merge branch 'webhook-events' of https://kolaente.dev/k3idii/api into webhook-events
continuous-integration/drone/pr Build is failing Details

This commit is contained in:
Ubuntu 2022-06-19 20:01:56 +00:00
commit 2cf73b07be
24 changed files with 290 additions and 95 deletions

2
go.mod
View File

@ -57,7 +57,7 @@ require (
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.11.0 github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.7.2 github.com/stretchr/testify v1.7.2
github.com/swaggo/swag v1.8.2 github.com/swaggo/swag v1.8.3
github.com/tkuchiki/go-timezone v0.2.2 github.com/tkuchiki/go-timezone v0.2.2
github.com/ulule/limiter/v3 v3.10.0 github.com/ulule/limiter/v3 v3.10.0
github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93 github.com/vectordotdev/go-datemath v0.1.1-0.20211214182920-0a4ac8742b93

2
go.sum
View File

@ -756,6 +756,8 @@ github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI=
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4= github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4=
github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg= github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
github.com/swaggo/swag v1.8.3 h1:3pZSSCQ//gAH88lfmxM3Cd1+JCsxV8Md6f36b9hrZ5s=
github.com/swaggo/swag v1.8.3/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q= github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=

View File

@ -63,6 +63,7 @@ func TestTaskComments(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
// Only the own comments can be updated
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "14", "commentid": "2"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "14", "commentid": "2"}, `{"comment":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
@ -74,14 +75,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "16", "commentid": "4"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "16", "commentid": "4"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "17", "commentid": "5"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "17", "commentid": "5"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
@ -90,14 +91,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "19", "commentid": "7"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "19", "commentid": "7"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "20", "commentid": "8"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "20", "commentid": "8"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
@ -106,14 +107,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "22", "commentid": "10"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "22", "commentid": "10"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "23", "commentid": "11"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "23", "commentid": "11"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
@ -122,14 +123,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "25", "commentid": "13"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "25", "commentid": "13"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "26", "commentid": "14"}, `{"comment":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"task": "26", "commentid": "14"}, `{"comment":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `"comment":"Lorem Ipsum"`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
}) })
}) })
@ -145,6 +146,7 @@ func TestTaskComments(t *testing.T) {
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
// Only the own comments can be deleted
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "14", "commentid": "2"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "14", "commentid": "2"})
assert.Error(t, err) assert.Error(t, err)
@ -156,14 +158,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "16", "commentid": "4"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "16", "commentid": "4"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "17", "commentid": "5"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "17", "commentid": "5"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
@ -172,14 +174,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "19", "commentid": "7"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "19", "commentid": "7"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "20", "commentid": "8"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "20", "commentid": "8"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
@ -188,14 +190,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "22", "commentid": "10"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "22", "commentid": "10"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "23", "commentid": "11"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "23", "commentid": "11"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
@ -204,14 +206,14 @@ func TestTaskComments(t *testing.T) {
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "25", "commentid": "13"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "25", "commentid": "13"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "26", "commentid": "14"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"task": "26", "commentid": "14"})
assert.NoError(t, err) assert.Error(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
}) })
}) })

View File

@ -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 <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type users20220616145228 struct {
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
}
func (users20220616145228) TableName() string {
return "users"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20220616145228",
Description: "Add overdue task summary time field to users",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(users20220616145228{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -43,6 +43,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View File

@ -54,6 +54,7 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -104,6 +105,7 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },
@ -168,6 +170,7 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -229,6 +232,7 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View File

@ -151,6 +151,7 @@ func TestListUser_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },
@ -164,6 +165,7 @@ func TestListUser_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View File

@ -150,6 +150,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },
@ -163,6 +164,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
}, },

View File

@ -37,6 +37,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -47,6 +48,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -57,6 +59,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }

View File

@ -27,16 +27,36 @@ func (tc *TaskComment) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
return t.CanRead(s, a) return t.CanRead(s, a)
} }
func (tc *TaskComment) canUserModifyTaskComment(s *xorm.Session, a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID}
canWriteTask, err := t.CanWrite(s, a)
if err != nil {
return false, err
}
if !canWriteTask {
return false, nil
}
savedComment := &TaskComment{
ID: tc.ID,
TaskID: tc.TaskID,
}
err = getTaskCommentSimple(s, savedComment)
if err != nil {
return false, err
}
return a.GetID() == savedComment.AuthorID, nil
}
// CanDelete checks if a user can delete a comment // CanDelete checks if a user can delete a comment
func (tc *TaskComment) CanDelete(s *xorm.Session, a web.Auth) (bool, error) { func (tc *TaskComment) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID} return tc.canUserModifyTaskComment(s, a)
return t.CanWrite(s, a)
} }
// CanUpdate checks if a user can update a comment // CanUpdate checks if a user can update a comment
func (tc *TaskComment) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) { func (tc *TaskComment) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID} return tc.canUserModifyTaskComment(s, a)
return t.CanWrite(s, a)
} }
// CanCreate checks if a user can create a new comment // CanCreate checks if a user can create a new comment

View File

@ -151,6 +151,24 @@ func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
}) })
} }
func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
exists, err := s.
Where("id = ? and task_id = ?", tc.ID, tc.TaskID).
NoAutoCondition().
Get(tc)
if err != nil {
return err
}
if !exists {
return ErrTaskCommentDoesNotExist{
ID: tc.ID,
TaskID: tc.TaskID,
}
}
return nil
}
// ReadOne handles getting a single comment // ReadOne handles getting a single comment
// @Summary Remove a task comment // @Summary Remove a task comment
// @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.
@ -166,15 +184,9 @@ func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [get] // @Router /tasks/{taskID}/comments/{commentID} [get]
func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) { func (tc *TaskComment) ReadOne(s *xorm.Session, a web.Auth) (err error) {
exists, err := s.Get(tc) err = getTaskCommentSimple(s, tc)
if err != nil { if err != nil {
return return err
}
if !exists {
return ErrTaskCommentDoesNotExist{
ID: tc.ID,
TaskID: tc.TaskID,
}
} }
// Get the author // Get the author

View File

@ -121,6 +121,16 @@ func TestTaskComment_Delete(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err)) assert.True(t, IsErrTaskCommentDoesNotExist(err))
}) })
t.Run("not the own comment", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
tc := &TaskComment{ID: 1, TaskID: 1}
can, err := tc.CanDelete(s, &user.User{ID: 2})
assert.NoError(t, err)
assert.False(t, can)
})
} }
func TestTaskComment_Update(t *testing.T) { func TestTaskComment_Update(t *testing.T) {
@ -157,6 +167,16 @@ func TestTaskComment_Update(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err)) assert.True(t, IsErrTaskCommentDoesNotExist(err))
}) })
t.Run("not the own comment", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
tc := &TaskComment{ID: 1, TaskID: 1}
can, err := tc.CanUpdate(s, &user.User{ID: 2})
assert.NoError(t, err)
assert.False(t, can)
})
} }
func TestTaskComment_ReadOne(t *testing.T) { func TestTaskComment_ReadOne(t *testing.T) {
@ -167,7 +187,7 @@ func TestTaskComment_ReadOne(t *testing.T) {
s := db.NewSession() s := db.NewSession()
defer s.Close() defer s.Close()
tc := &TaskComment{ID: 1} tc := &TaskComment{ID: 1, TaskID: 1}
err := tc.ReadOne(s, u) err := tc.ReadOne(s, u)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", tc.Comment) assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", tc.Comment)

View File

@ -32,7 +32,7 @@ import (
) )
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[int64]*userWithTasks, err error) { func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[int64]*userWithTasks, err error) {
now = utils.GetTimeWithoutNanoSeconds(now) now = utils.GetTimeWithoutSeconds(now)
nextMinute := now.Add(1 * time.Minute) nextMinute := now.Add(1 * time.Minute)
var tasks []*Task var tasks []*Task
@ -78,10 +78,14 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (usersWithTasks map[i
tzs[t.User.Timezone] = tz tzs[t.User.Timezone] = tz
} }
// If it is 9:00 for that current user, add the task to their list of overdue tasks // If it is time for that current user, add the task to their list of overdue tasks
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), 9, 0, 0, 0, tz) tm, err := time.Parse("15:04", t.User.OverdueTasksRemindersTime)
if err != nil {
return nil, err
}
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), tm.Hour(), tm.Minute(), 0, 0, tz)
isTimeForReminder := overdueMailTime.After(now) || overdueMailTime.Equal(now.In(tz)) isTimeForReminder := overdueMailTime.After(now) || overdueMailTime.Equal(now.In(tz))
wasTimeForReminder := overdueMailTime.Before(now.Add(time.Minute)) wasTimeForReminder := overdueMailTime.Before(nextMinute)
taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz)) taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz))
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone { if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
_, exists := uts[t.User.ID] _, exists := uts[t.User.ID]

View File

@ -61,7 +61,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
// Get all creators of tasks // Get all creators of tasks
creators := make(map[int64]*user.User, len(taskIDs)) creators := make(map[int64]*user.User, len(taskIDs))
err = s. err = s.
Select("users.id, users.username, users.email, users.name, users.timezone"). Select("users.*").
Join("LEFT", "tasks", "tasks.created_by_id = users.id"). Join("LEFT", "tasks", "tasks.created_by_id = users.id").
In("tasks.id", taskIDs). In("tasks.id", taskIDs).
Where(cond). Where(cond).

View File

@ -32,6 +32,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -42,6 +43,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -52,6 +54,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -63,6 +66,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -74,6 +78,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -84,6 +89,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -95,6 +101,7 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
DiscoverableByEmail: true, DiscoverableByEmail: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -105,6 +112,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -115,6 +123,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -125,6 +134,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -136,6 +146,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -148,6 +159,7 @@ func TestListUsersFromList(t *testing.T) {
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
DiscoverableByName: true, DiscoverableByName: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }
@ -158,6 +170,7 @@ func TestListUsersFromList(t *testing.T) {
Issuer: "local", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,
} }

View File

@ -17,6 +17,8 @@
package v1 package v1
import ( import (
"errors"
"fmt"
"net/http" "net/http"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -46,11 +48,13 @@ type UserSettings struct {
DiscoverableByEmail bool `json:"discoverable_by_email"` DiscoverableByEmail bool `json:"discoverable_by_email"`
// If enabled, the user will get an email for their overdue tasks each morning. // If enabled, the user will get an email for their overdue tasks each morning.
OverdueTasksRemindersEnabled bool `json:"overdue_tasks_reminders_enabled"` OverdueTasksRemindersEnabled bool `json:"overdue_tasks_reminders_enabled"`
// The time when the daily summary of overdue tasks will be sent via email.
OverdueTasksRemindersTime string `json:"overdue_tasks_reminders_time" valid:"time,required"`
// If a task is created without a specified list this value should be used. Applies // If a task is created without a specified list this value should be used. Applies
// to tasks made directly in API and from clients. // to tasks made directly in API and from clients.
DefaultListID int64 `json:"default_list_id"` DefaultListID int64 `json:"default_list_id"`
// The day when the week starts for this user. 0 = sunday, 1 = monday, etc. // The day when the week starts for this user. 0 = sunday, 1 = monday, etc.
WeekStart int `json:"week_start"` WeekStart int `json:"week_start" valid:"range(0|7)"`
// The user's language // The user's language
Language string `json:"language"` Language string `json:"language"`
// The user's time zone. Used to send task reminders in the time zone of the user. // The user's time zone. Used to send task reminders in the time zone of the user.
@ -158,7 +162,16 @@ func UpdateGeneralUserSettings(c echo.Context) error {
us := &UserSettings{} us := &UserSettings{}
err := c.Bind(us) err := c.Bind(us)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad user name provided.") var he *echo.HTTPError
if errors.As(err, &he) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid model provided. Error was: %s", he.Message))
}
return echo.NewHTTPError(http.StatusBadRequest, "Invalid model provided.")
}
err = c.Validate(us)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err)
} }
u, err := user2.GetCurrentUser(c) u, err := user2.GetCurrentUser(c)
@ -184,6 +197,7 @@ func UpdateGeneralUserSettings(c echo.Context) error {
user.WeekStart = us.WeekStart user.WeekStart = us.WeekStart
user.Language = us.Language user.Language = us.Language
user.Timezone = us.Timezone user.Timezone = us.Timezone
user.OverdueTasksRemindersTime = us.OverdueTasksRemindersTime
_, err = user2.UpdateUser(s, user) _, err = user2.UpdateUser(s, user)
if err != nil { if err != nil {

View File

@ -75,6 +75,7 @@ func UserShow(c echo.Context) error {
WeekStart: u.WeekStart, WeekStart: u.WeekStart,
Language: u.Language, Language: u.Language,
Timezone: u.Timezone, Timezone: u.Timezone,
OverdueTasksRemindersTime: u.OverdueTasksRemindersTime,
}, },
DeletionScheduledAt: u.DeletionScheduledAt, DeletionScheduledAt: u.DeletionScheduledAt,
IsLocalUser: u.Issuer == user.IssuerLocal, IsLocalUser: u.Issuer == user.IssuerLocal,

View File

@ -79,7 +79,6 @@ import (
"code.vikunja.io/web" "code.vikunja.io/web"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/asaskevich/govalidator"
"github.com/getsentry/sentry-go" "github.com/getsentry/sentry-go"
sentryecho "github.com/getsentry/sentry-go/echo" sentryecho "github.com/getsentry/sentry-go/echo"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
@ -88,31 +87,6 @@ import (
elog "github.com/labstack/gommon/log" elog "github.com/labstack/gommon/log"
) )
// CustomValidator is a dummy struct to use govalidator with echo
type CustomValidator struct{}
// Validate validates stuff
func (cv *CustomValidator) Validate(i interface{}) error {
if _, err := govalidator.ValidateStruct(i); err != nil {
var errs []string
for field, e := range govalidator.ErrorsByField(err) {
errs = append(errs, field+": "+e)
}
httperr := models.ValidationHTTPError{
HTTPError: web.HTTPError{
Code: models.ErrCodeInvalidData,
Message: "Invalid Data",
},
InvalidFields: errs,
}
return httperr
}
return nil
}
// NewEcho registers a new Echo instance // NewEcho registers a new Echo instance
func NewEcho() *echo.Echo { func NewEcho() *echo.Echo {
e := echo.New() e := echo.New()

55
pkg/routes/validation.go Normal file
View File

@ -0,0 +1,55 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package routes
import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/web"
"github.com/asaskevich/govalidator"
)
// CustomValidator is a dummy struct to use govalidator with echo
type CustomValidator struct{}
func init() {
govalidator.TagMap["time"] = govalidator.Validator(func(str string) bool {
return govalidator.IsTime(str, "15:04")
})
}
// Validate validates stuff
func (cv *CustomValidator) Validate(i interface{}) error {
if _, err := govalidator.ValidateStruct(i); err != nil {
var errs []string
for field, e := range govalidator.ErrorsByField(err) {
errs = append(errs, field+": "+e)
}
httperr := models.ValidationHTTPError{
HTTPError: web.HTTPError{
Code: models.ErrCodeInvalidData,
Message: "Invalid Data",
},
InvalidFields: errs,
}
return httperr
}
return nil
}

View File

@ -9184,6 +9184,10 @@ const docTemplate = `{
"description": "If enabled, the user will get an email for their overdue tasks each morning.", "description": "If enabled, the user will get an email for their overdue tasks each morning.",
"type": "boolean" "type": "boolean"
}, },
"overdue_tasks_reminders_time": {
"description": "The time when the daily summary of overdue tasks will be sent via email.",
"type": "string"
},
"timezone": { "timezone": {
"description": "The user's time zone. Used to send task reminders in the time zone of the user.", "description": "The user's time zone. Used to send task reminders in the time zone of the user.",
"type": "string" "type": "string"

View File

@ -9175,6 +9175,10 @@
"description": "If enabled, the user will get an email for their overdue tasks each morning.", "description": "If enabled, the user will get an email for their overdue tasks each morning.",
"type": "boolean" "type": "boolean"
}, },
"overdue_tasks_reminders_time": {
"description": "The time when the daily summary of overdue tasks will be sent via email.",
"type": "string"
},
"timezone": { "timezone": {
"description": "The user's time zone. Used to send task reminders in the time zone of the user.", "description": "The user's time zone. Used to send task reminders in the time zone of the user.",
"type": "string" "type": "string"

View File

@ -1310,6 +1310,10 @@ definitions:
description: If enabled, the user will get an email for their overdue tasks description: If enabled, the user will get an email for their overdue tasks
each morning. each morning.
type: boolean type: boolean
overdue_tasks_reminders_time:
description: The time when the daily summary of overdue tasks will be sent
via email.
type: string
timezone: timezone:
description: The user's time zone. Used to send task reminders in the time description: The user's time zone. Used to send task reminders in the time
zone of the user. zone of the user.

View File

@ -94,6 +94,7 @@ type User struct {
DiscoverableByName bool `xorm:"bool default false index" json:"-"` DiscoverableByName bool `xorm:"bool default false index" json:"-"`
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"` DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"` OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"`
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
DefaultListID int64 `xorm:"bigint null index" json:"-"` DefaultListID int64 `xorm:"bigint null index" json:"-"`
WeekStart int `xorm:"null" json:"-"` WeekStart int `xorm:"null" json:"-"`
Language string `xorm:"varchar(50) null" json:"-"` Language string `xorm:"varchar(50) null" json:"-"`
@ -493,6 +494,7 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) {
"week_start", "week_start",
"language", "language",
"timezone", "timezone",
"overdue_tasks_reminders_time",
). ).
Update(user) Update(user)
if err != nil { if err != nil {

View File

@ -30,3 +30,12 @@ func GetTimeWithoutNanoSeconds(t time.Time) time.Time {
// so we make sure the time we use to get the reminders don't contain nanoseconds. // 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) return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, t.Location()).In(tz)
} }
// GetTimeWithoutSeconds returns a time.Time with the seconds set to 0.
func GetTimeWithoutSeconds(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(), 0, 0, t.Location()).In(tz)
}