Compare commits

...

12 Commits

Author SHA1 Message Date
kolaente c0f58bb879 fix: use unique index for task #47 fixture
Task #47 and task #28 both had index 13 in project 1, producing
duplicate identifiers (test1-13). Change task #47 to index 32 to
avoid the collision.
2026-04-07 21:07:02 +02:00
kolaente a80d555264 fix: correct indentation in frontend files
Use tabs instead of spaces in IUserSettings.ts and fix alignment in
en.json to match surrounding code style.
2026-04-07 21:06:34 +02:00
kolaente e1728ee0bc fix: use double-quoted string for newline in todayLine
Raw backtick strings treat \n as literal characters, not a newline.
Switch the closing to a double-quoted string so the newline renders
correctly in the email.
2026-04-07 21:06:18 +02:00
kolaente cd5c1ba3e5 chore: remove generated config.yml.sample from version control
This file is generated by `mage generate:config-yaml` and should not
be committed.
2026-04-07 21:06:05 +02:00
kolaente 27c5876c60 chore: remove PLAN.md from version control
Plan files should not be committed to git per project conventions.
2026-04-07 21:06:00 +02:00
kolaente 1ff0b18080 fix(tests): update web test assertions for renamed task #47 fixture
Task #47 was renamed to "task #47 due today" with a due date, but the
webtests JSON assertions still referenced the old fixture. Update the
priority sort assertions to match the new task shape and insert task #47
into the duedate desc assertions where it now appears between tasks #28
and #5.
2026-04-05 19:44:55 +02:00
kolaente 503ca607d1 fix(plugins): update yaegi symbols for renamed reminder notification
After the rebase onto main (which added pkg/yaegi_symbols/), the renamed
DailyTasksReminderNotification (previously UndoneTask/UndoneTasksOverdueNotification)
needs to be reflected in the generated symbol table so yaegi plugins build.
2026-04-05 14:28:33 +02:00
kolaente 7c69302119 fix: resolve auto-merge issues from rebase
- Remove stale task47 reminder fixtures (task repurposed from "reminders
  outside window" to "due today")
- Fix raw SQL queries to use renamed today_tasks_reminders_time column
- Add missing today_tasks_reminders_enabled to SQL select clauses
- Remove task47 from "filtered with like" tests (title no longer contains "with")
- Fix e2e test to use renamed settings field
2026-04-05 14:24:46 +02:00
kolaente 0ccaf5b476 fix tests 2026-04-05 14:24:46 +02:00
kolaente 3109fa4a72 fix: add missing reminder translations 2026-04-05 14:23:20 +02:00
kolaente c5ddec0254 refactor: rename overdue reminders time field 2026-04-05 14:23:20 +02:00
kolaente 0e621dc38a fix: address review feedback for today reminders 2026-04-05 14:23:20 +02:00
28 changed files with 387 additions and 258 deletions

View File

@ -940,10 +940,15 @@
"default_value": "true",
"comment": "If set to true will send an email every day with all overdue tasks at a configured time."
},
{
"key": "today_tasks_reminders_enabled",
"default_value": "false",
"comment": "If set to true, include tasks due today in the daily overdue reminder email."
},
{
"key": "overdue_tasks_reminders_time",
"key": "today_tasks_reminders_time",
"default_value": "9:00",
"comment": "When to send the overdue task reminder email."
"comment": "When to send the daily tasks reminder email."
},
{
"key": "default_project_id",

View File

@ -97,6 +97,7 @@
"savedSuccess": "The settings were successfully updated.",
"emailReminders": "Send me reminders for tasks via email",
"overdueReminders": "Send me a summary of my undone overdue tasks every day",
"todayReminders": "Include tasks due today in reminder email",
"discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"playSoundWhenDone": "Play a sound when marking tasks as done",
@ -110,7 +111,7 @@
"defaultProject": "Default project",
"defaultView": "Default view",
"timezone": "Time zone",
"overdueTasksRemindersTime": "Overdue tasks reminder email time",
"todayTasksRemindersTime": "Tasks reminder email time",
"filterUsedOnOverview": "Saved filter used on the overview page",
"minimumPriority": "Minimum visible task priority",
"dateDisplay": "Date display format",

View File

@ -42,7 +42,8 @@ export interface IUserSettings extends IAbstract {
discoverableByName: boolean
discoverableByEmail: boolean
overdueTasksRemindersEnabled: boolean
overdueTasksRemindersTime: undefined | string | Date
todayTasksRemindersTime: undefined | string | Date
todayTasksRemindersEnabled: boolean
defaultProjectId: undefined | IProject['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string

View File

@ -15,7 +15,8 @@ export default class UserSettingsModel extends AbstractModel<IUserSettings> impl
discoverableByName = false
discoverableByEmail = false
overdueTasksRemindersEnabled = true
overdueTasksRemindersTime = undefined
todayTasksRemindersTime = undefined
todayTasksRemindersEnabled = false
defaultProjectId = undefined
weekStart = 0 as IUserSettings['weekStart']
timezone = ''

View File

@ -123,26 +123,34 @@
{{ $t('user.settings.general.overdueReminders') }}
</label>
</div>
<div
v-if="settings.overdueTasksRemindersEnabled"
class="field"
>
<label
for="overdueTasksReminderTime"
class="two-col"
>
<span>
{{ $t('user.settings.general.overdueTasksRemindersTime') }}
</span>
<div class="field">
<label class="checkbox">
<input
id="overdueTasksReminderTime"
v-model="settings.overdueTasksRemindersTime"
class="input"
type="time"
@keyup.enter="updateSettings"
v-model="settings.todayTasksRemindersEnabled"
type="checkbox"
>
{{ $t('user.settings.general.todayReminders') }}
</label>
</div>
<template v-if="settings.overdueTasksRemindersEnabled || settings.todayTasksRemindersEnabled">
<div class="field">
<label
for="todayTasksReminderTime"
class="two-col"
>
<span>
{{ $t('user.settings.general.todayTasksRemindersTime') }}
</span>
<input
id="todayTasksReminderTime"
v-model="settings.todayTasksRemindersTime"
class="input"
type="time"
@keyup.enter="updateSettings"
>
</label>
</div>
</template>
</div>
</Card>

View File

@ -127,7 +127,7 @@ test.describe('Home Page Task Overview', () => {
await updateUserSettings(apiContext, token, {
default_project_id: project.id,
overdue_tasks_reminders_time: '9:00',
today_tasks_reminders_time: '9:00',
})
const newTaskTitle = 'New Task'

View File

@ -208,11 +208,12 @@ const (
DefaultSettingsDiscoverableByName Key = `defaultsettings.discoverable_by_name`
DefaultSettingsDiscoverableByEmail Key = `defaultsettings.discoverable_by_email`
DefaultSettingsOverdueTaskRemindersEnabled Key = `defaultsettings.overdue_tasks_reminders_enabled`
DefaultSettingsTodayTasksRemindersEnabled Key = `defaultsettings.today_tasks_reminders_enabled`
DefaultSettingsDefaultProjectID Key = `defaultsettings.default_project_id`
DefaultSettingsWeekStart Key = `defaultsettings.week_start`
DefaultSettingsLanguage Key = `defaultsettings.language`
DefaultSettingsTimezone Key = `defaultsettings.timezone`
DefaultSettingsOverdueTaskRemindersTime Key = `defaultsettings.overdue_tasks_reminders_time`
DefaultSettingsTodayTaskRemindersTime Key = `defaultsettings.today_tasks_reminders_time`
WebhooksEnabled Key = `webhooks.enabled`
WebhooksTimeoutSeconds Key = `webhooks.timeoutseconds`
@ -473,7 +474,7 @@ func InitDefaultConfig() {
// Settings
DefaultSettingsAvatarProvider.setDefault("initials")
DefaultSettingsOverdueTaskRemindersEnabled.setDefault(true)
DefaultSettingsOverdueTaskRemindersTime.setDefault("9:00")
DefaultSettingsTodayTaskRemindersTime.setDefault("9:00")
// Webhook
WebhooksEnabled.setDefault(true)
WebhooksTimeoutSeconds.setDefault(30)

View File

@ -24,14 +24,3 @@
task_id: 2
reminder: 2019-06-01 12:00:00
created: 2018-12-01 01:12:04
# Task 47: two reminders, neither inside the (2018-10-01, 2018-12-10) window
# Reminder before the window:
- id: 6
task_id: 47
reminder: 2018-08-01 12:00:00
created: 2018-12-01 01:12:04
# Reminder after the window:
- id: 7
task_id: 47
reminder: 2019-03-01 12:00:00
created: 2018-12-01 01:12:04

View File

@ -412,12 +412,15 @@
due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
# Task due later today for reminders
- id: 47
title: 'task #47 with reminders outside window'
title: 'task #47 due today'
done: false
created_by_id: 1
project_id: 1
index: 32
due_date: 2018-12-01 23:00:00
created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04
- id: 48

View File

@ -7,6 +7,7 @@
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
export_file_id: 1
today_tasks_reminders_enabled: true
-
id: 2
username: 'user2'

View File

@ -68,7 +68,10 @@
"task": {
"reminder": {
"subject": "Reminder for \"%[1]s\" (%[2]s)",
"message": "This is a friendly reminder of the task \"%[1]s\" (%[2]s)."
"message": "This is a friendly reminder of the task \"%[1]s\" (%[2]s).",
"only_due_today_subject": "Tasks due today",
"overdue_intro": "You have the following overdue tasks:",
"today_intro": "You have the following tasks due today:"
},
"comment": {
"subject": "Re: %[1]s (%[2]s)",

View File

@ -0,0 +1,40 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
// usersTodayTasksRemindersEnabled20250903072808 adds the today_tasks_reminders_enabled column to the users table.
type usersTodayTasksRemindersEnabled20250903072808 struct {
TodayTasksRemindersEnabled bool `xorm:"not null default false index"`
}
func (usersTodayTasksRemindersEnabled20250903072808) TableName() string { return "users" }
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20250903072808",
Description: "Add today_tasks_reminders_enabled setting",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(usersTodayTasksRemindersEnabled20250903072808{})
},
Rollback: func(tx *xorm.Engine) error { return nil },
})
}

View File

@ -0,0 +1,45 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present 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 License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"xorm.io/xorm"
"xorm.io/xorm/schemas"
"src.techknowlogick.com/xormigrate"
)
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20250903072809",
Description: "Rename overdue_tasks_reminders_time column to today_tasks_reminders_time",
Migrate: func(tx *xorm.Engine) error {
switch tx.Dialect().URI().DBType {
case schemas.SQLITE:
_, err := tx.Exec("ALTER TABLE `users` RENAME COLUMN `overdue_tasks_reminders_time` TO `today_tasks_reminders_time`")
return err
case schemas.MYSQL:
_, err := tx.Exec("ALTER TABLE `users` CHANGE `overdue_tasks_reminders_time` `today_tasks_reminders_time` VARCHAR(5) NOT NULL DEFAULT '09:00'")
return err
default: // postgres
_, err := tx.Exec("ALTER TABLE \"users\" RENAME COLUMN \"overdue_tasks_reminders_time\" TO \"today_tasks_reminders_time\"")
return err
}
},
Rollback: func(tx *xorm.Engine) error { return nil },
})
}

View File

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

View File

@ -55,7 +55,8 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersEnabled: true,
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
ExportFileID: 1,
@ -67,7 +68,7 @@ func TestLabel_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
@ -177,7 +178,8 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersEnabled: true,
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
ExportFileID: 1,
@ -240,7 +242,7 @@ func TestLabel_ReadOne(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,

View File

@ -295,83 +295,72 @@ func getOverdueSinceString(until time.Duration, language string) (overdueSince s
}
// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification
type UndoneTaskOverdueNotification struct {
User *user.User
Task *Task
Project *Project
type DailyTasksReminderNotification struct {
User *user.User
OverdueTasks map[int64]*Task
DueToday map[int64]*Task
Projects map[int64]*Project
}
// ToMail returns the mail notification for UndoneTaskOverdueNotification
func (n *UndoneTaskOverdueNotification) ToMail(lang string) *notifications.Mail {
until := time.Until(n.Task.DueDate).Round(1*time.Hour) * -1
return notifications.NewMail().
IncludeLinkToSettings(lang).
Subject(i18n.T(lang, "notifications.task.overdue.subject", n.Task.Title, n.Project.Title)).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
Line(i18n.T(lang, "notifications.task.overdue.message", n.Task.Title, n.Project.Title, getOverdueSinceString(until, n.User.Language))).
Action(i18n.T(lang, "notifications.common.actions.open_task"), config.ServicePublicURL.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
Line(i18n.T(lang, "notifications.common.have_nice_day"))
}
// ToMail returns the mail notification for DailyTasksReminderNotification
func (n *DailyTasksReminderNotification) ToMail(lang string) *notifications.Mail {
// ToDB returns the UndoneTaskOverdueNotification notification in a format which can be saved in the db
func (n *UndoneTaskOverdueNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *UndoneTaskOverdueNotification) Name() string {
return "task.undone.overdue"
}
// ThreadID returns the thread ID for email threading
func (n *UndoneTaskOverdueNotification) ThreadID() string {
return getThreadID(n.Task.ID)
}
// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification
type UndoneTasksOverdueNotification struct {
User *user.User
Tasks map[int64]*Task
Projects map[int64]*Project
}
// ToMail returns the mail notification for UndoneTasksOverdueNotification
func (n *UndoneTasksOverdueNotification) ToMail(lang string) *notifications.Mail {
sortedTasks := make([]*Task, 0, len(n.Tasks))
for _, task := range n.Tasks {
sortedTasks = append(sortedTasks, task)
sortedOverdue := make([]*Task, 0, len(n.OverdueTasks))
for _, task := range n.OverdueTasks {
sortedOverdue = append(sortedOverdue, task)
}
sort.Slice(sortedOverdue, func(i, j int) bool {
return sortedOverdue[i].DueDate.Before(sortedOverdue[j].DueDate)
})
sort.Slice(sortedTasks, func(i, j int) bool {
return sortedTasks[i].DueDate.Before(sortedTasks[j].DueDate)
sortedToday := make([]*Task, 0, len(n.DueToday))
for _, task := range n.DueToday {
sortedToday = append(sortedToday, task)
}
sort.Slice(sortedToday, func(i, j int) bool {
return sortedToday[i].DueDate.Before(sortedToday[j].DueDate)
})
overdueLine := ""
for _, task := range sortedTasks {
for _, task := range sortedOverdue {
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
overdueLine += `* [` + task.Title + `](` + config.ServicePublicURL.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `) (` + n.Projects[task.ProjectID].Title + `), ` + i18n.T("notifications.task.overdue.overdue", getOverdueSinceString(until, n.User.Language)) + "\n"
}
return notifications.NewMail().
todayLine := ""
for _, task := range sortedToday {
todayLine += `* [` + task.Title + `](` + config.ServicePublicURL.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `) (` + n.Projects[task.ProjectID].Title + ")\n"
}
subject := i18n.T(lang, "notifications.task.overdue.multiple_subject")
if len(n.OverdueTasks) == 0 {
subject = i18n.T(lang, "notifications.task.reminder.only_due_today_subject")
}
m := notifications.NewMail().
IncludeLinkToSettings(lang).
Subject(i18n.T(lang, "notifications.task.overdue.multiple_subject")).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())).
Line(i18n.T(lang, "notifications.task.overdue.multiple_message")).
Line(overdueLine).
Action(i18n.T(lang, "notifications.common.actions.open_vikunja"), config.ServicePublicURL.GetString()).
Subject(subject).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName()))
if overdueLine != "" {
m.Line(i18n.T(lang, "notifications.task.reminder.overdue_intro"))
m.Line(overdueLine)
}
if todayLine != "" {
m.Line(i18n.T(lang, "notifications.task.reminder.today_intro"))
m.Line(todayLine)
}
m.Action(i18n.T(lang, "notifications.common.actions.open_vikunja"), config.ServicePublicURL.GetString()).
Line(i18n.T(lang, "notifications.common.have_nice_day"))
return m
}
// ToDB returns the UndoneTasksOverdueNotification notification in a format which can be saved in the db
func (n *UndoneTasksOverdueNotification) ToDB() interface{} {
return nil
}
// ToDB returns the DailyTasksReminderNotification notification in a format which can be saved in the db
func (n *DailyTasksReminderNotification) ToDB() interface{} { return nil }
// Name returns the name of the notification
func (n *UndoneTasksOverdueNotification) Name() string {
return "task.undone.overdue"
}
func (n *DailyTasksReminderNotification) Name() string { return "task.daily.reminder" }
// UserMentionedInTaskNotification represents a UserMentionedInTaskNotification notification
type UserMentionedInTaskNotification struct {

View File

@ -153,7 +153,8 @@ func TestProjectUser_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersEnabled: true,
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
ExportFileID: 1,
@ -168,7 +169,7 @@ func TestProjectUser_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,

View File

@ -43,7 +43,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersEnabled: true,
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
ExportFileID: 1,
@ -55,7 +56,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersEnabled: false,
TodayTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
@ -67,7 +69,8 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersEnabled: false,
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -606,30 +609,17 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Updated: time.Unix(1543626724, 0).In(loc),
}
task47 := &Task{
ID: 47,
Title: "task #47 with reminders outside window",
Identifier: "test1-32",
Index: 32,
CreatedByID: 1,
CreatedBy: user1,
Reminders: []*TaskReminder{
{
ID: 6,
TaskID: 47,
Reminder: time.Date(2018, 8, 1, 12, 0, 0, 0, loc),
Created: time.Unix(1543626724, 0).In(loc),
},
{
ID: 7,
TaskID: 47,
Reminder: time.Date(2019, 3, 1, 12, 0, 0, 0, loc),
Created: time.Unix(1543626724, 0).In(loc),
},
},
ID: 47,
Title: "task #47 due today",
Identifier: "test1-32",
Index: 32,
CreatedByID: 1,
CreatedBy: user1,
ProjectID: 1,
DueDate: time.Date(2018, 12, 1, 23, 0, 0, 0, loc),
RelatedTasks: map[RelationKind][]*Task{},
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
Created: time.Date(2018, 12, 1, 1, 12, 4, 0, loc),
Updated: time.Date(2018, 12, 1, 1, 12, 4, 0, loc),
}
task48 := &Task{
ID: 48,
@ -1002,7 +992,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task32, // has nil dates
task33, // has nil dates
task39, // has nil dates
task47, // has nil dates
task47,
task48, // has nil dates
},
wantErr: false,
@ -1035,7 +1025,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task30,
task31,
task33,
task47,
},
wantErr: false,
},
@ -1055,7 +1044,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task30,
task31,
task33,
task47,
},
wantErr: false,
},
@ -1071,18 +1059,6 @@ func TestTaskCollection_ReadAll(t *testing.T) {
},
wantErr: false,
},
{
name: "filtered reminder dates should not match task with reminders outside window",
fields: fields{
Filter: "reminders > '2018-10-01T00:00:00+00:00' && reminders < '2018-12-10T00:00:00+00:00'",
},
args: defaultArgs,
want: []*Task{
task2,
task27,
},
wantErr: false,
},
{
name: "filtered reminder dates narrow window excludes all",
fields: fields{
@ -1099,8 +1075,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
},
args: defaultArgs,
want: []*Task{
task2, // has reminder at 2019-06-01 (> 2019-01-01)
task47, // has reminder at 2019-03-01 (> 2019-01-01) and 2018-08-01 (< 2018-09-01)
task2, // has reminder at 2019-06-01 (> 2019-01-01)
},
wantErr: false,
},
@ -1601,10 +1576,10 @@ func TestTaskCollection_ReadAll(t *testing.T) {
// The only tasks with a due date
task6,
task5,
task47,
task28,
// The other ones don't have a due date
task48,
task47,
task39,
task33,
task32,
@ -1652,6 +1627,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
task7,
task6,
task5,
task47,
task28,
},
},
@ -1667,6 +1643,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
},
want: []*Task{
task28,
task47,
task5,
task6,
task7,
@ -1687,6 +1664,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
want: []*Task{
task6,
task5,
task47,
task28,
task7,
task8,

View File

@ -158,7 +158,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
creators := []*userWithTask{}
err = s.Table("tasks").
Select("DISTINCT tasks.id AS task_id, users.id, users.name, users.username, users.email, users.email_reminders_enabled, users.overdue_tasks_reminders_enabled, users.overdue_tasks_reminders_time, users.language, users.timezone, users.created, users.updated").
Select("DISTINCT tasks.id AS task_id, users.id, users.name, users.username, users.email, users.email_reminders_enabled, users.overdue_tasks_reminders_enabled, users.today_tasks_reminders_enabled, users.today_tasks_reminders_time, users.language, users.timezone, users.created, users.updated").
Join("INNER", "users", "tasks.created_by_id = users.id").
Where(builder.And(conditions...)).
Find(&creators)
@ -182,7 +182,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
assignees := []*TaskAssigneeWithUser{}
err = s.Table("task_assignees").
Select("DISTINCT task_assignees.task_id, users.id, users.name, users.username, users.email, users.email_reminders_enabled, users.overdue_tasks_reminders_enabled, users.overdue_tasks_reminders_time, users.language, users.timezone, users.created, users.updated").
Select("DISTINCT task_assignees.task_id, users.id, users.name, users.username, users.email, users.email_reminders_enabled, users.overdue_tasks_reminders_enabled, users.today_tasks_reminders_enabled, users.today_tasks_reminders_time, users.language, users.timezone, users.created, users.updated").
Join("INNER", "users", "task_assignees.user_id = users.id").
Where(builder.And(assigneeConds...)).
Find(&assignees)

View File

@ -32,13 +32,13 @@ import (
"xorm.io/xorm"
)
func getUndoneOverdueTasks(s *xorm.Session, now time.Time, cond builder.Cond) (usersWithTasks map[int64]*userWithTasks, err error) {
func getTasksForDailyReminder(s *xorm.Session, now time.Time, cond builder.Cond) (usersWithTasks map[int64]*userWithTasks, err error) {
now = utils.GetTimeWithoutSeconds(now)
nextMinute := now.Add(1 * time.Minute)
var tasks []*Task
err = s.
Where("due_date is not null AND due_date < ? AND projects.is_archived = false", nextMinute.Add(time.Hour*14).Format(dbTimeFormat)).
Where("due_date is not null AND due_date < ? AND projects.is_archived = false", nextMinute.Add(time.Hour*38).Format(dbTimeFormat)).
Join("LEFT", "projects", "projects.id = tasks.project_id").
And("done = false").
Find(&tasks)
@ -81,23 +81,32 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time, cond builder.Cond) (u
}
// If it is time for that current user, add the task to their project of overdue tasks
tm, err := time.Parse("15:04", t.User.OverdueTasksRemindersTime)
tm, err := time.Parse("15:04", t.User.TodayTasksRemindersTime)
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 {
reminderTime := time.Date(now.Year(), now.Month(), now.Day(), tm.Hour(), tm.Minute(), 0, 0, tz)
isTimeForReminder := reminderTime.After(now) || reminderTime.Equal(now.In(tz))
wasTimeForReminder := reminderTime.Before(nextMinute)
taskDue := t.Task.DueDate.In(tz)
endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, tz)
if isTimeForReminder && wasTimeForReminder {
_, exists := uts[t.User.ID]
if !exists {
uts[t.User.ID] = &userWithTasks{
user: t.User,
tasks: make(map[int64]*Task),
user: t.User,
overdue: make(map[int64]*Task),
dueToday: make(map[int64]*Task),
}
}
uts[t.User.ID].tasks[t.Task.ID] = t.Task
if t.User.OverdueTasksRemindersEnabled && reminderTime.After(taskDue) {
uts[t.User.ID].overdue[t.Task.ID] = t.Task
continue
}
if t.User.TodayTasksRemindersEnabled && taskDue.After(reminderTime) && taskDue.Before(endOfDay) {
uts[t.User.ID].dueToday[t.Task.ID] = t.Task
}
}
}
@ -105,11 +114,12 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time, cond builder.Cond) (u
}
type userWithTasks struct {
user *user.User
tasks map[int64]*Task
user *user.User
overdue map[int64]*Task
dueToday map[int64]*Task
}
// RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done.
// RegisterOverdueReminderCron registers a function which checks once a day for overdue tasks and tasks due today and sends reminders.
func RegisterOverdueReminderCron() {
webhookEnabled := config.WebhooksEnabled.GetBool()
emailEnabled := config.ServiceEnableEmailReminders.GetBool() && config.MailerEnabled.GetBool()
@ -130,12 +140,15 @@ func RegisterOverdueReminderCron() {
var cond builder.Cond
if emailEnabled && !webhookEnabled {
cond = builder.Eq{"users.overdue_tasks_reminders_enabled": true}
cond = builder.Or(
builder.Eq{"users.overdue_tasks_reminders_enabled": true},
builder.Eq{"users.today_tasks_reminders_enabled": true},
)
}
uts, err := getUndoneOverdueTasks(s, now, cond)
uts, err := getTasksForDailyReminder(s, now, cond)
if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not get undone overdue tasks in the next minute: %s", err)
log.Errorf("[Daily Tasks Reminder] Could not get tasks for daily reminder: %s", err)
return
}
@ -143,80 +156,92 @@ func RegisterOverdueReminderCron() {
return
}
log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(uts))
log.Debugf("[Daily Tasks Reminder] Sending reminders to %d users", len(uts))
taskIDs := []int64{}
for _, ut := range uts {
for _, t := range ut.tasks {
for _, t := range ut.overdue {
taskIDs = append(taskIDs, t.ID)
}
for _, t := range ut.dueToday {
taskIDs = append(taskIDs, t.ID)
}
}
projects, err := GetProjectsMapSimpleByTaskIDs(s, taskIDs)
if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not get projects for tasks: %s", err)
log.Errorf("[Daily Tasks Reminder] Could not get projects for tasks: %s", err)
return
}
for _, ut := range uts {
if emailEnabled && ut.user.OverdueTasksRemindersEnabled {
var n notifications.Notification = &UndoneTasksOverdueNotification{
User: ut.user,
Tasks: ut.tasks,
Projects: projects,
}
if len(ut.tasks) == 1 {
for _, t := range ut.tasks {
n = &UndoneTaskOverdueNotification{
User: ut.user,
Task: t,
Project: projects[t.ProjectID],
}
// Dispatch webhook events, deduplicated by task ID across all users
if webhookEnabled {
dispatchedTasks := make(map[int64]bool)
for _, ut := range uts {
// Per-task overdue events
for _, t := range ut.overdue {
if dispatchedTasks[t.ID] {
continue
}
}
err = notifications.Notify(ut.user, n, s)
if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not notify user %d: %s", ut.user.ID, err)
return
}
}
// Dispatch webhook events
if webhookEnabled {
// Per-task events
for _, t := range ut.tasks {
dispatchedTasks[t.ID] = true
err = events.Dispatch(&TaskOverdueEvent{
Task: t,
User: ut.user,
Project: projects[t.ProjectID],
})
if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not dispatch overdue event for task %d: %s", t.ID, err)
log.Errorf("[Daily Tasks Reminder] Could not dispatch overdue event for task %d: %s", t.ID, err)
}
}
// Batch event
err = events.Dispatch(&TasksOverdueEvent{
Tasks: mapToSlice(ut.tasks),
User: ut.user,
Projects: projects,
})
if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not dispatch batch overdue event for user %d: %s", ut.user.ID, err)
if len(ut.overdue) > 0 {
err = events.Dispatch(&TasksOverdueEvent{
Tasks: mapToSlice(ut.overdue),
User: ut.user,
Projects: projects,
})
if err != nil {
log.Errorf("[Daily Tasks Reminder] Could not dispatch batch overdue event for user %d: %s", ut.user.ID, err)
}
}
}
}
log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder for %d tasks to user %d", len(ut.tasks), ut.user.ID)
if !emailEnabled {
if err := s.Commit(); err != nil {
log.Errorf("[Daily Tasks Reminder] Could not commit: %s", err)
}
return
}
for _, ut := range uts {
if len(ut.overdue) == 0 && len(ut.dueToday) == 0 {
continue
}
n := &DailyTasksReminderNotification{
User: ut.user,
OverdueTasks: ut.overdue,
DueToday: ut.dueToday,
Projects: projects,
}
err = notifications.Notify(ut.user, n, s)
if err != nil {
log.Errorf("[Daily Tasks Reminder] Could not notify user %d: %s", ut.user.ID, err)
return
}
log.Debugf("[Daily Tasks Reminder] Sent reminder email to user %d (overdue: %d, today: %d)", ut.user.ID, len(ut.overdue), len(ut.dueToday))
}
if err := s.Commit(); err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not commit: %s", err)
log.Errorf("[Daily Tasks Reminder] Could not commit: %s", err)
}
})
if err != nil {
log.Fatalf("Could not register undone overdue tasks reminder cron: %s", err)
log.Fatalf("Could not register daily tasks reminder cron: %s", err)
}
}

View File

@ -27,7 +27,7 @@ import (
"xorm.io/builder"
)
func TestGetUndoneOverDueTasks(t *testing.T) {
func TestGetTasksForDailyReminder(t *testing.T) {
t.Run("no undone tasks", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
@ -35,34 +35,61 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
require.NoError(t, err)
tasks, err := getUndoneOverdueTasks(s, now, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
tasks, err := getTasksForDailyReminder(s, now, builder.Or(
builder.Eq{"users.overdue_tasks_reminders_enabled": true},
builder.Eq{"users.today_tasks_reminders_enabled": true},
))
require.NoError(t, err)
assert.Empty(t, tasks)
})
t.Run("undone overdue", func(t *testing.T) {
t.Run("overdue and due today", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z")
require.NoError(t, err)
uts, err := getUndoneOverdueTasks(s, now, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
uts, err := getTasksForDailyReminder(s, now, builder.Or(
builder.Eq{"users.overdue_tasks_reminders_enabled": true},
builder.Eq{"users.today_tasks_reminders_enabled": true},
))
require.NoError(t, err)
require.Len(t, uts, 1)
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")
assert.Len(t, uts, 1)
assert.Len(t, uts[1].overdue, 2)
assert.Len(t, uts[1].dueToday, 1)
_, ok := uts[1].dueToday[47]
assert.True(t, ok)
// Disable today reminders and ensure the task is not included
_, err = s.Where("id = ?", 1).Cols("today_tasks_reminders_enabled").Update(&user.User{TodayTasksRemindersEnabled: false})
require.NoError(t, err)
uts, err = getTasksForDailyReminder(s, now, builder.Or(
builder.Eq{"users.overdue_tasks_reminders_enabled": true},
builder.Eq{"users.today_tasks_reminders_enabled": true},
))
require.NoError(t, err)
assert.Len(t, uts[1].overdue, 2)
assert.Empty(t, uts[1].dueToday)
})
t.Run("only due today", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// disable overdue reminders, keep today reminders enabled
_, err := s.Where("id = ?", 1).Cols("overdue_tasks_reminders_enabled").Update(&user.User{OverdueTasksRemindersEnabled: false})
require.NoError(t, err)
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z")
require.NoError(t, err)
uts, err := getTasksForDailyReminder(s, now, builder.Or(
builder.Eq{"users.overdue_tasks_reminders_enabled": true},
builder.Eq{"users.today_tasks_reminders_enabled": true},
))
require.NoError(t, err)
assert.Len(t, uts, 1)
assert.Empty(t, uts[1].overdue)
assert.Len(t, uts[1].dueToday, 1)
})
t.Run("done overdue", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -71,7 +98,10 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
require.NoError(t, err)
tasks, err := getUndoneOverdueTasks(s, now, builder.Eq{"users.overdue_tasks_reminders_enabled": true})
tasks, err := getTasksForDailyReminder(s, now, builder.Or(
builder.Eq{"users.overdue_tasks_reminders_enabled": true},
builder.Eq{"users.today_tasks_reminders_enabled": true},
))
require.NoError(t, err)
assert.Empty(t, tasks)
})

View File

@ -32,7 +32,8 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersEnabled: true,
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
ExportFileID: 1,
@ -44,7 +45,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
@ -56,7 +57,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
DefaultProjectID: 4,
Created: testCreatedTime,
Updated: testUpdatedTime,
@ -69,7 +70,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -81,7 +82,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -92,7 +93,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -104,7 +105,7 @@ func TestListUsersFromProject(t *testing.T) {
EmailRemindersEnabled: true,
DiscoverableByEmail: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -115,7 +116,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -126,7 +127,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -137,7 +138,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -149,7 +150,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -162,7 +163,7 @@ func TestListUsersFromProject(t *testing.T) {
EmailRemindersEnabled: true,
DiscoverableByName: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}
@ -173,7 +174,7 @@ func TestListUsersFromProject(t *testing.T) {
Issuer: "local",
EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00",
TodayTasksRemindersTime: "09:00",
Created: testCreatedTime,
Updated: testUpdatedTime,
}

View File

@ -48,8 +48,10 @@ type UserSettings struct {
DiscoverableByEmail bool `json:"discoverable_by_email"`
// If enabled, the user will get an email for their overdue tasks each morning.
OverdueTasksRemindersEnabled bool `json:"overdue_tasks_reminders_enabled"`
// 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 enabled, includes tasks due later today in the overdue reminder email.
TodayTasksRemindersEnabled bool `json:"today_tasks_reminders_enabled"`
// The time when the daily summary of tasks will be sent via email.
TodayTasksRemindersTime string `json:"today_tasks_reminders_time" valid:"time,required"`
// If a task is created without a specified project this value should be used. Applies
// to tasks made directly in API and from clients.
DefaultProjectID int64 `json:"default_project_id"`
@ -209,11 +211,12 @@ func UpdateGeneralUserSettings(c *echo.Context) error {
user.DiscoverableByEmail = us.DiscoverableByEmail
user.DiscoverableByName = us.DiscoverableByName
user.OverdueTasksRemindersEnabled = us.OverdueTasksRemindersEnabled
user.TodayTasksRemindersEnabled = us.TodayTasksRemindersEnabled
user.DefaultProjectID = us.DefaultProjectID
user.WeekStart = us.WeekStart
user.Language = us.Language
user.Timezone = us.Timezone
user.OverdueTasksRemindersTime = us.OverdueTasksRemindersTime
user.TodayTasksRemindersTime = us.TodayTasksRemindersTime
user.FrontendSettings = us.FrontendSettings
_, err = user2.UpdateUser(s, user, true)

View File

@ -77,7 +77,7 @@ func UserShow(c *echo.Context) error {
WeekStart: u.WeekStart,
Language: u.Language,
Timezone: u.Timezone,
OverdueTasksRemindersTime: u.OverdueTasksRemindersTime,
TodayTasksRemindersTime: u.TodayTasksRemindersTime,
FrontendSettings: u.FrontendSettings,
ExtraSettingsLinks: u.ExtraSettingsLinks,
},

View File

@ -105,7 +105,8 @@ type User struct {
DiscoverableByName bool `xorm:"bool default false index" json:"-"`
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"`
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
TodayTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"`
TodayTasksRemindersEnabled bool `xorm:"bool default false index" json:"-"`
DefaultProjectID int64 `xorm:"bigint null index" json:"-"`
WeekStart int `xorm:"null" json:"-"`
Language string `xorm:"varchar(50) null" json:"-" valid:"language"`
@ -317,8 +318,8 @@ func getUser(s *xorm.Session, user *User, withEmail bool) (userOut *User, err er
userOut.Email = ""
}
if userOut.OverdueTasksRemindersTime == "" {
userOut.OverdueTasksRemindersTime = "9:00"
if userOut.TodayTasksRemindersTime == "" {
userOut.TodayTasksRemindersTime = "9:00"
}
if userOut.Status == StatusDisabled {
@ -629,11 +630,12 @@ func UpdateUser(s *xorm.Session, user *User, forceOverride bool) (updatedUser *U
"discoverable_by_name",
"discoverable_by_email",
"overdue_tasks_reminders_enabled",
"today_tasks_reminders_enabled",
"default_project_id",
"week_start",
"language",
"timezone",
"overdue_tasks_reminders_time",
"today_tasks_reminders_time",
"frontend_settings",
"extra_settings_links",
).

View File

@ -67,7 +67,8 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) {
user.DiscoverableByName = config.DefaultSettingsDiscoverableByName.GetBool()
user.DiscoverableByEmail = config.DefaultSettingsDiscoverableByEmail.GetBool()
user.OverdueTasksRemindersEnabled = config.DefaultSettingsOverdueTaskRemindersEnabled.GetBool()
user.OverdueTasksRemindersTime = config.DefaultSettingsOverdueTaskRemindersTime.GetString()
user.TodayTasksRemindersTime = config.DefaultSettingsTodayTaskRemindersTime.GetString()
user.TodayTasksRemindersEnabled = config.DefaultSettingsTodayTasksRemindersEnabled.GetBool()
user.DefaultProjectID = config.DefaultSettingsDefaultProjectID.GetInt64()
user.WeekStart = config.DefaultSettingsWeekStart.GetInt()
user.Timezone = config.DefaultSettingsTimezone.GetString()

View File

@ -132,7 +132,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 with reminders outside window","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":[{"reminder":"2018-08-01T12:00:00Z","relative_period":0,"relative_to":""},{"reminder":"2019-03-01T12:00:00Z","relative_period":0,"relative_to":""}],"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 due today","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T23:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
@ -142,7 +142,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 with reminders outside window","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":[{"reminder":"2018-08-01T12:00:00Z","relative_period":0,"relative_to":""},{"reminder":"2019-03-01T12:00:00Z","relative_period":0,"relative_to":""}],"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 due today","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T23:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
// should equal duedate asc
t.Run("by due_date", func(t *testing.T) {
@ -153,7 +153,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":28,"title":"task #28 with repeat after, start_date, end_date and due_date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-02T22:25:24Z","reminders":null,"project_id":1,"repeat_after":3600,"repeat_mode":0,"priority":0,"start_date":"2018-11-30T22:25:24Z","end_date":"2018-12-13T11:20:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-13","index":13,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"id":28,"title":"task #28 with repeat after, start_date, end_date and due_date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-02T22:25:24Z","reminders":null,"project_id":1,"repeat_after":3600,"repeat_mode":0,"priority":0,"start_date":"2018-11-30T22:25:24Z","end_date":"2018-12-13T11:20:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-13","index":13,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 due today","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T23:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
})
// Due date without unix suffix
t.Run("by duedate asc without suffix", func(t *testing.T) {
@ -169,7 +169,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by duedate desc without suffix", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":28,"title":"task #28 with repeat after, start_date, end_date and due_date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-02T22:25:24Z","reminders":null,"project_id":1,"repeat_after":3600,"repeat_mode":0,"priority":0,"start_date":"2018-11-30T22:25:24Z","end_date":"2018-12-13T11:20:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-13","index":13,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"id":28,"title":"task #28 with repeat after, start_date, end_date and due_date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-02T22:25:24Z","reminders":null,"project_id":1,"repeat_after":3600,"repeat_mode":0,"priority":0,"start_date":"2018-11-30T22:25:24Z","end_date":"2018-12-13T11:20:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-13","index":13,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 due today","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T23:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
})
t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, urlParams)
@ -380,7 +380,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 with reminders outside window","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":[{"reminder":"2018-08-01T12:00:00Z","relative_period":0,"relative_to":""},{"reminder":"2019-03-01T12:00:00Z","relative_period":0,"relative_to":""}],"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 due today","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T23:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
@ -390,7 +390,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 with reminders outside window","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":[{"reminder":"2018-08-01T12:00:00Z","relative_period":0,"relative_to":""},{"reminder":"2019-03-01T12:00:00Z","relative_period":0,"relative_to":""}],"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
assert.Contains(t, rec.Body.String(), `{"id":33,"title":"task #33 with percent done","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":39,"title":"task #39","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":25,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"#0","index":0,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 due today","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T23:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":48,"title":"Landingpages update","description":"Update all landingpages with new branding","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"0001-01-01T00:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-33","index":33,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}}]`)
})
// should equal duedate asc
t.Run("by due_date", func(t *testing.T) {
@ -401,7 +401,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":28,"title":"task #28 with repeat after, start_date, end_date and due_date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-02T22:25:24Z","reminders":null,"project_id":1,"repeat_after":3600,"repeat_mode":0,"priority":0,"start_date":"2018-11-30T22:25:24Z","end_date":"2018-12-13T11:20:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-13","index":13,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
assert.Contains(t, rec.Body.String(), `[{"id":28,"title":"task #28 with repeat after, start_date, end_date and due_date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-02T22:25:24Z","reminders":null,"project_id":1,"repeat_after":3600,"repeat_mode":0,"priority":0,"start_date":"2018-11-30T22:25:24Z","end_date":"2018-12-13T11:20:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-13","index":13,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":47,"title":"task #47 due today","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T23:00:00Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-32","index":32,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":5,"title":"task #5 higher due date","description":"","done":false,"done_at":"0001-01-01T00:00:00Z","due_date":"2018-12-01T03:58:44Z","reminders":null,"project_id":1,"repeat_after":0,"repeat_mode":0,"priority":0,"start_date":"0001-01-01T00:00:00Z","end_date":"0001-01-01T00:00:00Z","assignees":null,"labels":null,"hex_color":"","percent_done":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"cover_image_attachment_id":0,"is_favorite":false,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","bucket_id":0,"position":0,"reactions":null,"created_by":{"id":1,"name":"","username":"user1","created":"2018-12-01T15:13:12Z","updated":"2018-12-02T15:13:12Z"}},{"id":6,"title":"task #6 lower due date`)
})
t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"asc"}}, nil)

View File

@ -20,6 +20,7 @@ func init() {
"CreateNewProjectForUser": reflect.ValueOf(models.CreateNewProjectForUser),
"CreateProject": reflect.ValueOf(models.CreateProject),
"CreateSession": reflect.ValueOf(models.CreateSession),
"DailyTasksReminderNotification": reflect.ValueOf((*models.DailyTasksReminderNotification)(nil)),
"DeleteAllUserSessions": reflect.ValueOf(models.DeleteAllUserSessions),
"DeleteOrphanedTaskPositions": reflect.ValueOf(models.DeleteOrphanedTaskPositions),
"DeleteUser": reflect.ValueOf(models.DeleteUser),
@ -501,8 +502,6 @@ func init() {
"TeamProject": reflect.ValueOf((*models.TeamProject)(nil)),
"TeamUser": reflect.ValueOf((*models.TeamUser)(nil)),
"TeamWithPermission": reflect.ValueOf((*models.TeamWithPermission)(nil)),
"UndoneTaskOverdueNotification": reflect.ValueOf((*models.UndoneTaskOverdueNotification)(nil)),
"UndoneTasksOverdueNotification": reflect.ValueOf((*models.UndoneTasksOverdueNotification)(nil)),
"UnsplashPhoto": reflect.ValueOf((*models.UnsplashPhoto)(nil)),
"UpdateTaskInSavedFilterViews": reflect.ValueOf((*models.UpdateTaskInSavedFilterViews)(nil)),
"UserDataExportRequestedEvent": reflect.ValueOf((*models.UserDataExportRequestedEvent)(nil)),