From 61d49c3a56a59e52ce407b858ddd4aa573dbee9d Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 16 Jan 2022 11:05:56 +0000 Subject: [PATCH] feat: add time zone setting for reminders (#1092) Instead of naeveily checking for all reminders due in the next minute, we now check all reminders in all time zones in the next minutes. This essentially means checking for reminders due in the next 14 or past 12 hours. We then check for each user who would receive a reminder from that result if it is actually due in their time zone. This should prevent issues where users would get the reminder in the time zone of their server, not in their own. Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/1092 Co-authored-by: konrad Co-committed-by: konrad --- go.mod | 1 + go.sum | 39 +------------ pkg/migration/20220112211537.go | 50 +++++++++++++++++ pkg/models/task_reminder.go | 88 +++++++++++++++++++++--------- pkg/models/task_reminder_test.go | 8 +-- pkg/routes/api/v1/user_settings.go | 36 +++++++++++- pkg/routes/api/v1/user_show.go | 1 + pkg/routes/routes.go | 1 + pkg/swagger/docs.go | 41 ++++++++++++++ pkg/swagger/swagger.json | 41 ++++++++++++++ pkg/swagger/swagger.yaml | 30 ++++++++++ pkg/user/user.go | 12 ++++ 12 files changed, 279 insertions(+), 69 deletions(-) create mode 100644 pkg/migration/20220112211537.go diff --git a/go.mod b/go.mod index 59420201ff..16d0e1a9ea 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/spf13/viper v1.10.1 github.com/stretchr/testify v1.7.0 github.com/swaggo/swag v1.7.8 + github.com/tkuchiki/go-timezone v0.2.2 // indirect github.com/ulule/limiter/v3 v3.9.0 github.com/yuin/goldmark v1.4.4 golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce diff --git a/go.sum b/go.sum index 10808c3128..e34d00f6d9 100644 --- a/go.sum +++ b/go.sum @@ -29,7 +29,6 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= -cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= @@ -49,7 +48,6 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0 h1:6RRlFMv1omScs6iq2hfE3IvgE+l6RfJPampq8UZc5TU= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3 h1:MXl7Ff9a/ndTpuEmQKIGhqReE9hWhD4T/+AzK4AXUYc= code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3/go.mod h1:OgFO06HN1KpA4S7Dw/QAIeygiUPSeGJJn1ykz/sjZdU= @@ -236,17 +234,11 @@ github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AE github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= -github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -286,7 +278,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -338,11 +329,9 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -366,9 +355,7 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -518,10 +505,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awStJ6ArI7Y= -github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M= -github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k= -github.com/labstack/echo/v4 v4.6.2 h1:lGl58LRvItiofInOQGHHLuH2TyGU3BAEgmEv55N65nM= -github.com/labstack/echo/v4 v4.6.2/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A= github.com/labstack/echo/v4 v4.6.3 h1:VhPuIZYxsbPmo4m9KAkMU/el2442eB7EBFFhNTTT9ac= github.com/labstack/echo/v4 v4.6.3/go.mod h1:Hk5OiHj0kDqmFq7aHe7eDqI7CUhuCrfpupQtLGGLm7A= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= @@ -579,8 +562,6 @@ github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71 github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= -github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -726,10 +707,6 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.7.0 h1:xc1yh8vgcNB8yQ+UqY4cpD56Ogo573e+CJ/C4YmMFTg= -github.com/spf13/afero v1.7.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.7.1 h1:F37zV8E8RLstLpZ0RUGK2NGg1X57y6/B0Eg6S8oqdoA= -github.com/spf13/afero v1.7.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= @@ -760,12 +737,12 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/swaggo/swag v1.7.6 h1:UbAqHyXkW2J+cDjs5S43MkuYR7a6stB7Am7SK8NBmRg= -github.com/swaggo/swag v1.7.6/go.mod h1:7vLqNYEtYoIsD14wXgy9oDS65MNiDANrPtbk9rnLuj0= github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc= github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= 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/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -815,7 +792,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -849,8 +825,6 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI= golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1066,17 +1040,12 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe h1:W8vbETX/n8S6EmY0Pu4Ix7VvpsJUESTwl0oCK8MJOgk= -golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= @@ -1164,7 +1133,6 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= @@ -1208,7 +1176,6 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= -google.golang.org/api v0.63.0 h1:n2bqqK895ygnBpdPDYetfy23K7fJ22wsrZKCyfuRkkA= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1284,7 +1251,6 @@ google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa h1:I0YcKz0I7OAhddo7ya8kMnvprhcWM045PmkBdMO9zN0= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1314,7 +1280,6 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/pkg/migration/20220112211537.go b/pkg/migration/20220112211537.go new file mode 100644 index 0000000000..8037a96172 --- /dev/null +++ b/pkg/migration/20220112211537.go @@ -0,0 +1,50 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package migration + +import ( + "code.vikunja.io/api/pkg/config" + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type users20220112211537 struct { + Timezone string `xorm:"varchar(255) null" json:"-"` +} + +func (users20220112211537) TableName() string { + return "users" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20220112211537", + Description: "Add time zone setting for users", + Migrate: func(tx *xorm.Engine) error { + err := tx.Sync2(users20220112211537{}) + if err != nil { + return err + } + + _, err = tx.Update(&users20220112211537{Timezone: config.GetTimeZone().String()}) + return err + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/task_reminder.go b/pkg/models/task_reminder.go index 19365ed107..620d1ab9ec 100644 --- a/pkg/models/task_reminder.go +++ b/pkg/models/task_reminder.go @@ -61,11 +61,11 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) ( // Get all creators of tasks creators := make(map[int64]*user.User, len(taskIDs)) err = s. - Select("users.id, users.username, users.email, users.name"). + Select("users.id, users.username, users.email, users.name, users.timezone"). Join("LEFT", "tasks", "tasks.created_by_id = users.id"). In("tasks.id", taskIDs). Where(cond). - GroupBy("tasks.id, users.id, users.username, users.email, users.name"). + GroupBy("tasks.id, users.id, users.username, users.email, users.name, users.timezone"). Find(&creators) if err != nil { return @@ -77,14 +77,14 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) ( return } - for _, taskID := range taskIDs { - u, exists := creators[taskMap[taskID].CreatedByID] + for _, task := range taskMap { + u, exists := creators[task.CreatedByID] if !exists { continue } taskUsers = append(taskUsers, &taskUser{ - Task: taskMap[taskID], + Task: taskMap[task.ID], User: u, }) } @@ -110,8 +110,9 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) ( return } -func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskIDs []int64, err error) { +func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (reminderNotifications []*ReminderDueNotification, err error) { now = utils.GetTimeWithoutNanoSeconds(now) + reminderNotifications = []*ReminderDueNotification{} nextMinute := now.Add(1 * time.Minute) @@ -120,7 +121,8 @@ func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskI reminders := []*TaskReminder{} err = s. Join("INNER", "tasks", "tasks.id = task_reminders.task_id"). - Where("reminder >= ? and reminder < ?", now.Format(dbTimeFormat), nextMinute.Format(dbTimeFormat)). + // All reminders from -12h to +14h to include all time zones + Where("reminder >= ? and reminder < ?", now.Add(time.Hour*-12).Format(dbTimeFormat), nextMinute.Add(time.Hour*14).Format(dbTimeFormat)). And("tasks.done = false"). Find(&reminders) if err != nil { @@ -133,11 +135,56 @@ func getTasksWithRemindersInTheNextMinute(s *xorm.Session, now time.Time) (taskI return } - // We're sending a reminder to everyone who is assigned to the task or has created it. + var taskIDs []int64 for _, r := range reminders { taskIDs = append(taskIDs, r.TaskID) } + if len(taskIDs) == 0 { + return + } + + usersWithReminders, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true}) + if err != nil { + return + } + + usersPerTask := make(map[int64][]*taskUser, len(usersWithReminders)) + for _, ur := range usersWithReminders { + usersPerTask[ur.Task.ID] = append(usersPerTask[ur.Task.ID], ur) + } + + // Time zone cache per time zone string to avoid parsing the same time zone over and over again + tzs := make(map[string]*time.Location) + // Figure out which reminders are actually due in the time zone of the users + for _, r := range reminders { + + for _, u := range usersPerTask[r.TaskID] { + + if u.User.Timezone == "" { + u.User.Timezone = config.GetTimeZone().String() + } + + // I think this will break once there's more reminders than what we can handle in one minute + tz, exists := tzs[u.User.Timezone] + if !exists { + tz, err = time.LoadLocation(u.User.Timezone) + if err != nil { + return + } + tzs[u.User.Timezone] = tz + } + + actualReminder := r.Reminder.In(tz) + if (actualReminder.After(now) && actualReminder.Before(now.Add(time.Minute))) || actualReminder.Equal(now) { + reminderNotifications = append(reminderNotifications, &ReminderDueNotification{ + User: u.User, + Task: u.Task, + }) + } + } + } + return } @@ -162,37 +209,26 @@ func RegisterReminderCron() { defer s.Close() now := time.Now() - taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now) + reminders, err := getTasksWithRemindersDueAndTheirUsers(s, now) if err != nil { log.Errorf("[Task Reminder Cron] Could not get tasks with reminders in the next minute: %s", err) return } - if len(taskIDs) == 0 { + if len(reminders) == 0 { return } - users, err := getTaskUsersForTasks(s, taskIDs, builder.Eq{"users.email_reminders_enabled": true}) - if err != nil { - log.Errorf("[Task Reminder Cron] Could not get task users to send them reminders: %s", err) - return - } + log.Debugf("[Task Reminder Cron] Sending %d reminders", len(reminders)) - log.Debugf("[Task Reminder Cron] Sending reminders to %d users", len(users)) - - for _, u := range users { - n := &ReminderDueNotification{ - User: u.User, - Task: u.Task, - } - - err = notifications.Notify(u.User, n) + for _, n := range reminders { + err = notifications.Notify(n.User, n) if err != nil { - log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", u.User.ID, err) + log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", n.User.ID, err) return } - log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID) + log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", n.Task.ID, n.User.ID) } }) if err != nil { diff --git a/pkg/models/task_reminder_test.go b/pkg/models/task_reminder_test.go index 2d447062ab..12eefe2c2f 100644 --- a/pkg/models/task_reminder_test.go +++ b/pkg/models/task_reminder_test.go @@ -32,10 +32,10 @@ func TestReminderGetTasksInTheNextMinute(t *testing.T) { now, err := time.Parse(time.RFC3339Nano, "2018-12-01T01:13:00Z") assert.NoError(t, err) - taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now) + notifications, err := getTasksWithRemindersDueAndTheirUsers(s, now) assert.NoError(t, err) - assert.Len(t, taskIDs, 1) - assert.Equal(t, int64(27), taskIDs[0]) + assert.Len(t, notifications, 1) + assert.Equal(t, int64(27), notifications[0].Task.ID) }) t.Run("Found No Tasks", func(t *testing.T) { db.LoadAndAssertFixtures(t) @@ -44,7 +44,7 @@ func TestReminderGetTasksInTheNextMinute(t *testing.T) { now, err := time.Parse(time.RFC3339Nano, "2018-12-02T01:13:00Z") assert.NoError(t, err) - taskIDs, err := getTasksWithRemindersInTheNextMinute(s, now) + taskIDs, err := getTasksWithRemindersDueAndTheirUsers(s, now) assert.NoError(t, err) assert.Len(t, taskIDs, 0) }) diff --git a/pkg/routes/api/v1/user_settings.go b/pkg/routes/api/v1/user_settings.go index ceed1e1707..157160aad7 100644 --- a/pkg/routes/api/v1/user_settings.go +++ b/pkg/routes/api/v1/user_settings.go @@ -19,12 +19,13 @@ package v1 import ( "net/http" - "code.vikunja.io/api/pkg/db" + "github.com/labstack/echo/v4" + "github.com/tkuchiki/go-timezone" + "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/models" user2 "code.vikunja.io/api/pkg/user" "code.vikunja.io/web/handler" - "github.com/labstack/echo/v4" ) // UserAvatarProvider holds the user avatar provider type @@ -52,6 +53,8 @@ type UserSettings struct { WeekStart int `json:"week_start"` // The user's language Language string `json:"language"` + // The user's time zone. Used to send task reminders in the time zone of the user. + Timezone string `json:"timezone"` } // GetUserAvatarProvider returns the currently set user avatar @@ -180,6 +183,7 @@ func UpdateGeneralUserSettings(c echo.Context) error { user.DefaultListID = us.DefaultListID user.WeekStart = us.WeekStart user.Language = us.Language + user.Timezone = us.Timezone _, err = user2.UpdateUser(s, user) if err != nil { @@ -194,3 +198,31 @@ func UpdateGeneralUserSettings(c echo.Context) error { return c.JSON(http.StatusOK, &models.Message{Message: "The settings were updated successfully."}) } + +// GetAvailableTimezones +// @Summary Get all available time zones on this vikunja instance +// @Description Because available time zones depend on the system Vikunja is running on, this endpoint returns a list of all valid time zones this particular Vikunja instance can handle. The list of time zones is not sorted, you should sort it on the client. +// @tags user +// @Accept json +// @Produce json +// @Security JWTKeyAuth +// @Success 200 {array} string "All available time zones." +// @Failure 500 {object} models.Message "Internal server error." +// @Router /user/timezones [get] +func GetAvailableTimezones(c echo.Context) error { + + allTimezones := timezone.New().Timezones() + timezoneMap := make(map[string]bool) // to filter all duplicates + for _, s := range allTimezones { + for _, t := range s { + timezoneMap[t] = true + } + } + + ts := []string{} + for s := range timezoneMap { + ts = append(ts, s) + } + + return c.JSON(http.StatusOK, ts) +} diff --git a/pkg/routes/api/v1/user_show.go b/pkg/routes/api/v1/user_show.go index 902918f9a0..9bcdb348a8 100644 --- a/pkg/routes/api/v1/user_show.go +++ b/pkg/routes/api/v1/user_show.go @@ -74,6 +74,7 @@ func UserShow(c echo.Context) error { DefaultListID: u.DefaultListID, WeekStart: u.WeekStart, Language: u.Language, + Timezone: u.Timezone, }, DeletionScheduledAt: u.DeletionScheduledAt, IsLocalUser: u.Issuer == user.IssuerLocal, diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 044f3a306e..b37387c5b7 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -321,6 +321,7 @@ func registerAPIRoutes(a *echo.Group) { u.POST("/settings/general", apiv1.UpdateGeneralUserSettings) u.POST("/export/request", apiv1.RequestUserDataExport) u.POST("/export/download", apiv1.DownloadUserDataExport) + u.GET("/timezones", apiv1.GetAvailableTimezones) if config.ServiceEnableTotp.GetBool() { u.GET("/settings/totp", apiv1.UserTOTP) diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index a893660b71..08487fd0a0 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7260,6 +7260,43 @@ var doc = `{ } } }, + "/user/timezones": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Because available time zones depend on the system Vikunja is running on, this endpoint returns a list of all valid time zones this particular Vikunja instance can handle. The list of time zones is not sorted, you should sort it on the client.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get all available time zones on this vikunja instance", + "responses": { + "200": { + "description": "All available time zones.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/user/token": { "post": { "description": "Returns a new valid jwt user token with an extended length.", @@ -8978,6 +9015,10 @@ var doc = `{ "description": "If enabled, the user will get an email for their overdue tasks each morning.", "type": "boolean" }, + "timezone": { + "description": "The user's time zone. Used to send task reminders in the time zone of the user.", + "type": "string" + }, "week_start": { "description": "The day when the week starts for this user. 0 = sunday, 1 = monday, etc.", "type": "integer" diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 7822e8696f..fde0490ec3 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7244,6 +7244,43 @@ } } }, + "/user/timezones": { + "get": { + "security": [ + { + "JWTKeyAuth": [] + } + ], + "description": "Because available time zones depend on the system Vikunja is running on, this endpoint returns a list of all valid time zones this particular Vikunja instance can handle. The list of time zones is not sorted, you should sort it on the client.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get all available time zones on this vikunja instance", + "responses": { + "200": { + "description": "All available time zones.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error.", + "schema": { + "$ref": "#/definitions/models.Message" + } + } + } + } + }, "/user/token": { "post": { "description": "Returns a new valid jwt user token with an extended length.", @@ -8962,6 +8999,10 @@ "description": "If enabled, the user will get an email for their overdue tasks each morning.", "type": "boolean" }, + "timezone": { + "description": "The user's time zone. Used to send task reminders in the time zone of the user.", + "type": "string" + }, "week_start": { "description": "The day when the week starts for this user. 0 = sunday, 1 = monday, etc.", "type": "integer" diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 45f40b81b0..e5d3eb6337 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -1284,6 +1284,10 @@ definitions: description: If enabled, the user will get an email for their overdue tasks each morning. type: boolean + timezone: + description: The user's time zone. Used to send task reminders in the time + zone of the user. + type: string week_start: description: The day when the week starts for this user. 0 = sunday, 1 = monday, etc. @@ -6212,6 +6216,32 @@ paths: summary: Totp QR Code tags: - user + /user/timezones: + get: + consumes: + - application/json + description: Because available time zones depend on the system Vikunja is running + on, this endpoint returns a list of all valid time zones this particular Vikunja + instance can handle. The list of time zones is not sorted, you should sort + it on the client. + produces: + - application/json + responses: + "200": + description: All available time zones. + schema: + items: + type: string + type: array + "500": + description: Internal server error. + schema: + $ref: '#/definitions/models.Message' + security: + - JWTKeyAuth: [] + summary: Get all available time zones on this vikunja instance + tags: + - user /user/token: post: consumes: diff --git a/pkg/user/user.go b/pkg/user/user.go index 44d6ad246b..fb7cff6645 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -95,6 +95,7 @@ type User struct { DefaultListID int64 `xorm:"bigint null index" json:"-"` WeekStart int `xorm:"null" json:"-"` Language string `xorm:"varchar(50) null" json:"-"` + Timezone string `xorm:"varchar(255) null" json:"-"` DeletionScheduledAt time.Time `xorm:"datetime null" json:"-"` DeletionLastReminderSent time.Time `xorm:"datetime null" json:"-"` @@ -462,6 +463,16 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) { } } + // Check if we have a valid time zone + if user.Timezone == "" { + user.Timezone = config.GetTimeZone().String() + } + + _, err = time.LoadLocation(user.Timezone) + if err != nil { + return + } + // Update it _, err = s. ID(user.ID). @@ -479,6 +490,7 @@ func UpdateUser(s *xorm.Session, user *User) (updatedUser *User, err error) { "default_list_id", "week_start", "language", + "timezone", ). Update(user) if err != nil {