forked from vikunja/vikunja
Compare commits
7 Commits
92a87cfe4f
...
01271c4c01
Author | SHA1 | Date | |
---|---|---|---|
01271c4c01 | |||
d837f8a624 | |||
8869adfc27 | |||
030bbfa47e | |||
7eb3b96a44 | |||
2f25b48869 | |||
|
172a6214d7 |
|
@ -56,6 +56,9 @@ service:
|
||||||
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
# it may be required to coordinate with them in order to delete the account. This setting will not affect the cli commands
|
||||||
# for user deletion.
|
# for user deletion.
|
||||||
enableuserdeletion: true
|
enableuserdeletion: true
|
||||||
|
# The maximum size clients will be able to request for user avatars.
|
||||||
|
# If clients request a size bigger than this, it will be changed on the fly.
|
||||||
|
maxavatarsize: 1024
|
||||||
|
|
||||||
database:
|
database:
|
||||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||||
|
|
|
@ -76,7 +76,7 @@ Default: `<jwt-secret>`
|
||||||
|
|
||||||
Full path: `service.JWTSecret`
|
Full path: `service.JWTSecret`
|
||||||
|
|
||||||
Environment path: `VIKUNJA_SERVICE_JWT_SECRET`
|
Environment path: `VIKUNJA_SERVICE_JWTSECRET`
|
||||||
|
|
||||||
|
|
||||||
### jwtttl
|
### jwtttl
|
||||||
|
@ -321,6 +321,18 @@ Full path: `service.enableuserdeletion`
|
||||||
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
|
Environment path: `VIKUNJA_SERVICE_ENABLEUSERDELETION`
|
||||||
|
|
||||||
|
|
||||||
|
### maxavatarsize
|
||||||
|
|
||||||
|
The maximum size clients will be able to request for user avatars.
|
||||||
|
If clients request a size bigger than this, it will be changed on the fly.
|
||||||
|
|
||||||
|
Default: `1024`
|
||||||
|
|
||||||
|
Full path: `service.maxavatarsize`
|
||||||
|
|
||||||
|
Environment path: `VIKUNJA_SERVICE_MAXAVATARSIZE`
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## database
|
## database
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||||
|
|
|
@ -1051,7 +1051,7 @@ func printConfig(config []*configOption, level int, parent string) (rendered str
|
||||||
fullPath := parent + "." + option.key
|
fullPath := parent + "." + option.key
|
||||||
|
|
||||||
rendered += "Full path: `" + fullPath + "`\n\n"
|
rendered += "Full path: `" + fullPath + "`\n\n"
|
||||||
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(fullPath) + "`\n\n"
|
rendered += "Environment path: `VIKUNJA_" + strcase.ToScreamingSnake(strings.ToUpper(fullPath)) + "`\n\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ const (
|
||||||
ServiceTestingtoken Key = `service.testingtoken`
|
ServiceTestingtoken Key = `service.testingtoken`
|
||||||
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
||||||
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
||||||
|
ServiceMaxAvatarSize Key = `service.maxavatarsize`
|
||||||
|
|
||||||
AuthLocalEnabled Key = `auth.local.enabled`
|
AuthLocalEnabled Key = `auth.local.enabled`
|
||||||
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
||||||
|
@ -287,6 +288,7 @@ func InitDefaultConfig() {
|
||||||
ServiceEnableTotp.setDefault(true)
|
ServiceEnableTotp.setDefault(true)
|
||||||
ServiceEnableEmailReminders.setDefault(true)
|
ServiceEnableEmailReminders.setDefault(true)
|
||||||
ServiceEnableUserDeletion.setDefault(true)
|
ServiceEnableUserDeletion.setDefault(true)
|
||||||
|
ServiceMaxAvatarSize.setDefault(1024)
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
AuthLocalEnabled.setDefault(true)
|
AuthLocalEnabled.setDefault(true)
|
||||||
|
|
|
@ -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`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
43
pkg/migration/20220616145228.go
Normal file
43
pkg/migration/20220616145228.go
Normal 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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -19,37 +19,89 @@ package models
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/user"
|
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/config"
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/cron"
|
"code.vikunja.io/api/pkg/cron"
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
"code.vikunja.io/api/pkg/log"
|
"code.vikunja.io/api/pkg/log"
|
||||||
"code.vikunja.io/api/pkg/notifications"
|
"code.vikunja.io/api/pkg/notifications"
|
||||||
|
"code.vikunja.io/api/pkg/user"
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, 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)
|
||||||
|
|
||||||
var tasks []*Task
|
var tasks []*Task
|
||||||
err = s.
|
err = s.
|
||||||
Where("due_date is not null and due_date < ?", now.Format(dbTimeFormat)).
|
Where("due_date is not null and due_date < ?", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
|
||||||
And("done = false").
|
And("done = false").
|
||||||
Find(&tasks)
|
Find(&tasks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var taskIDs []int64
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
taskIDs = append(taskIDs, task.ID)
|
taskIDs = append(taskIDs, task.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
|
||||||
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(users) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uts := make(map[int64]*userWithTasks)
|
||||||
|
tzs := make(map[string]*time.Location)
|
||||||
|
for _, t := range users {
|
||||||
|
if t.User.Timezone == "" {
|
||||||
|
t.User.Timezone = config.GetTimeZone().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
tz, exists := tzs[t.User.Timezone]
|
||||||
|
if !exists {
|
||||||
|
tz, err = time.LoadLocation(t.User.Timezone)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tzs[t.User.Timezone] = tz
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is time for that current user, add the task to their list of overdue tasks
|
||||||
|
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))
|
||||||
|
wasTimeForReminder := overdueMailTime.Before(nextMinute)
|
||||||
|
taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz))
|
||||||
|
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone {
|
||||||
|
_, exists := uts[t.User.ID]
|
||||||
|
if !exists {
|
||||||
|
uts[t.User.ID] = &userWithTasks{
|
||||||
|
user: t.User,
|
||||||
|
tasks: []*Task{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uts, nil
|
||||||
|
}
|
||||||
|
|
||||||
type userWithTasks struct {
|
type userWithTasks struct {
|
||||||
user *user.User
|
user *user.User
|
||||||
tasks []*Task
|
tasks []*Task
|
||||||
|
@ -66,36 +118,18 @@ func RegisterOverdueReminderCron() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := cron.Schedule("0 8 * * *", func() {
|
err := cron.Schedule("* * * * *", func() {
|
||||||
s := db.NewSession()
|
s := db.NewSession()
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
uts, err := getUndoneOverdueTasks(s, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get tasks with reminders in the next minute: %s", err)
|
log.Errorf("[Undone Overdue Tasks Reminder] Could not get undone overdue tasks in the next minute: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
|
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(uts))
|
||||||
if err != nil {
|
|
||||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get task users to send them reminders: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uts := make(map[int64]*userWithTasks)
|
|
||||||
for _, t := range users {
|
|
||||||
_, exists := uts[t.User.ID]
|
|
||||||
if !exists {
|
|
||||||
uts[t.User.ID] = &userWithTasks{
|
|
||||||
user: t.User,
|
|
||||||
tasks: []*Task{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users))
|
|
||||||
|
|
||||||
for _, ut := range uts {
|
for _, ut := range uts {
|
||||||
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
var n notifications.Notification = &UndoneTasksOverdueNotification{
|
||||||
|
@ -117,7 +151,6 @@ func RegisterOverdueReminderCron() {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID)
|
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -32,21 +32,34 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
|
||||||
|
|
||||||
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
|
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
tasks, err := getUndoneOverdueTasks(s, now)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, taskIDs, 0)
|
assert.Len(t, tasks, 0)
|
||||||
})
|
})
|
||||||
t.Run("undone overdue", func(t *testing.T) {
|
t.Run("undone overdue", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
s := db.NewSession()
|
s := db.NewSession()
|
||||||
defer s.Close()
|
defer s.Close()
|
||||||
|
|
||||||
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z")
|
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
uts, err := getUndoneOverdueTasks(s, now)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, taskIDs, 1)
|
assert.Len(t, uts, 1)
|
||||||
assert.Equal(t, int64(6), taskIDs[0])
|
assert.Len(t, uts[1].tasks, 2)
|
||||||
|
// The tasks don't always have the same order, so we only check their presence, not their position.
|
||||||
|
var task5Present bool
|
||||||
|
var task6Present bool
|
||||||
|
for _, t := range uts[1].tasks {
|
||||||
|
if t.ID == 5 {
|
||||||
|
task5Present = true
|
||||||
|
}
|
||||||
|
if t.ID == 6 {
|
||||||
|
task6Present = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Truef(t, task5Present, "expected task 5 to be present but was not")
|
||||||
|
assert.Truef(t, task6Present, "expected task 6 to be present but was not")
|
||||||
})
|
})
|
||||||
t.Run("done overdue", func(t *testing.T) {
|
t.Run("done overdue", func(t *testing.T) {
|
||||||
db.LoadAndAssertFixtures(t)
|
db.LoadAndAssertFixtures(t)
|
||||||
|
@ -55,8 +68,8 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
|
||||||
|
|
||||||
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
|
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
taskIDs, err := getUndoneOverdueTasks(s, now)
|
tasks, err := getUndoneOverdueTasks(s, now)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, taskIDs, 0)
|
assert.Len(t, tasks, 0)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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).
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/config"
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
"code.vikunja.io/api/pkg/files"
|
"code.vikunja.io/api/pkg/files"
|
||||||
"code.vikunja.io/api/pkg/log"
|
"code.vikunja.io/api/pkg/log"
|
||||||
|
@ -49,7 +50,7 @@ import (
|
||||||
// @tags user
|
// @tags user
|
||||||
// @Produce octet-stream
|
// @Produce octet-stream
|
||||||
// @Param username path string true "The username of the user who's avatar you want to get"
|
// @Param username path string true "The username of the user who's avatar you want to get"
|
||||||
// @Param size query int false "The size of the avatar you want to get"
|
// @Param size query int false "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size."
|
||||||
// @Success 200 {} blob "The avatar"
|
// @Success 200 {} blob "The avatar"
|
||||||
// @Failure 404 {object} models.Message "The user does not exist."
|
// @Failure 404 {object} models.Message "The user does not exist."
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
|
@ -97,6 +98,9 @@ func GetAvatar(c echo.Context) error {
|
||||||
return handler.HandleHTTPError(err, c)
|
return handler.HandleHTTPError(err, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if sizeInt > config.ServiceMaxAvatarSize.GetInt64() {
|
||||||
|
sizeInt = config.ServiceMaxAvatarSize.GetInt64()
|
||||||
|
}
|
||||||
|
|
||||||
// Get the avatar
|
// Get the avatar
|
||||||
a, mimeType, err := avatarProvider.GetAvatar(u, sizeInt)
|
a, mimeType, err := avatarProvider.GetAvatar(u, sizeInt)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
55
pkg/routes/validation.go
Normal 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
|
||||||
|
}
|
|
@ -7537,7 +7537,7 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The size of the avatar you want to get",
|
"description": "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size.",
|
||||||
"name": "size",
|
"name": "size",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -7528,7 +7528,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The size of the avatar you want to get",
|
"description": "The size of the avatar you want to get. If bigger than the max configured size this will be adjusted to the maximum size.",
|
||||||
"name": "size",
|
"name": "size",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -1433,7 +1437,8 @@ paths:
|
||||||
name: username
|
name: username
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
- description: The size of the avatar you want to get
|
- description: The size of the avatar you want to get. If bigger than the max
|
||||||
|
configured size this will be adjusted to the maximum size.
|
||||||
in: query
|
in: query
|
||||||
name: size
|
name: size
|
||||||
type: integer
|
type: integer
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user