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

@ -941,9 +941,14 @@
"comment": "If set to true will send an email every day with all overdue tasks at a configured time." "comment": "If set to true will send an email every day with all overdue tasks at a configured time."
}, },
{ {
"key": "overdue_tasks_reminders_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": "today_tasks_reminders_time",
"default_value": "9:00", "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", "key": "default_project_id",

View File

@ -97,6 +97,7 @@
"savedSuccess": "The settings were successfully updated.", "savedSuccess": "The settings were successfully updated.",
"emailReminders": "Send me reminders for tasks via email", "emailReminders": "Send me reminders for tasks via email",
"overdueReminders": "Send me a summary of my undone overdue tasks every day", "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", "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", "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", "playSoundWhenDone": "Play a sound when marking tasks as done",
@ -110,7 +111,7 @@
"defaultProject": "Default project", "defaultProject": "Default project",
"defaultView": "Default view", "defaultView": "Default view",
"timezone": "Time zone", "timezone": "Time zone",
"overdueTasksRemindersTime": "Overdue tasks reminder email time", "todayTasksRemindersTime": "Tasks reminder email time",
"filterUsedOnOverview": "Saved filter used on the overview page", "filterUsedOnOverview": "Saved filter used on the overview page",
"minimumPriority": "Minimum visible task priority", "minimumPriority": "Minimum visible task priority",
"dateDisplay": "Date display format", "dateDisplay": "Date display format",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,14 +24,3 @@
task_id: 2 task_id: 2
reminder: 2019-06-01 12:00:00 reminder: 2019-06-01 12:00:00
created: 2018-12-01 01:12:04 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 due_date: 2023-03-01 15:00:00
created: 2018-12-01 01:12:04 created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04
# Task due later today for reminders
- id: 47 - id: 47
title: 'task #47 with reminders outside window' title: 'task #47 due today'
done: false done: false
created_by_id: 1 created_by_id: 1
project_id: 1 project_id: 1
index: 32 index: 32
due_date: 2018-12-01 23:00:00
created: 2018-12-01 01:12:04 created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04
- id: 48 - id: 48

View File

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

View File

@ -68,7 +68,10 @@
"task": { "task": {
"reminder": { "reminder": {
"subject": "Reminder for \"%[1]s\" (%[2]s)", "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": { "comment": {
"subject": "Re: %[1]s (%[2]s)", "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", Issuer: "local",
EmailRemindersEnabled: true, EmailRemindersEnabled: true,
OverdueTasksRemindersEnabled: true, OverdueTasksRemindersEnabled: true,
OverdueTasksRemindersTime: "09:00", TodayTasksRemindersTime: "09:00",
DefaultProjectID: 4, DefaultProjectID: 4,
Created: testCreatedTime, Created: testCreatedTime,
Updated: testUpdatedTime, Updated: testUpdatedTime,

View File

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

View File

@ -295,83 +295,72 @@ func getOverdueSinceString(until time.Duration, language string) (overdueSince s
} }
// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification // UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification
type UndoneTaskOverdueNotification struct { type DailyTasksReminderNotification struct {
User *user.User User *user.User
Task *Task OverdueTasks map[int64]*Task
Project *Project DueToday map[int64]*Task
}
// 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"))
}
// 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 Projects map[int64]*Project
} }
// ToMail returns the mail notification for UndoneTasksOverdueNotification // ToMail returns the mail notification for DailyTasksReminderNotification
func (n *UndoneTasksOverdueNotification) ToMail(lang string) *notifications.Mail { func (n *DailyTasksReminderNotification) ToMail(lang string) *notifications.Mail {
sortedTasks := make([]*Task, 0, len(n.Tasks)) sortedOverdue := make([]*Task, 0, len(n.OverdueTasks))
for _, task := range n.Tasks { for _, task := range n.OverdueTasks {
sortedTasks = append(sortedTasks, task) 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 { sortedToday := make([]*Task, 0, len(n.DueToday))
return sortedTasks[i].DueDate.Before(sortedTasks[j].DueDate) 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 := "" overdueLine := ""
for _, task := range sortedTasks { for _, task := range sortedOverdue {
until := time.Until(task.DueDate).Round(1*time.Hour) * -1 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" 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). IncludeLinkToSettings(lang).
Subject(i18n.T(lang, "notifications.task.overdue.multiple_subject")). Subject(subject).
Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName())). Greeting(i18n.T(lang, "notifications.greeting", n.User.GetName()))
Line(i18n.T(lang, "notifications.task.overdue.multiple_message")).
Line(overdueLine). if overdueLine != "" {
Action(i18n.T(lang, "notifications.common.actions.open_vikunja"), config.ServicePublicURL.GetString()). 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")) 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 // ToDB returns the DailyTasksReminderNotification notification in a format which can be saved in the db
func (n *UndoneTasksOverdueNotification) ToDB() interface{} { func (n *DailyTasksReminderNotification) ToDB() interface{} { return nil }
return nil
}
// Name returns the name of the notification // Name returns the name of the notification
func (n *UndoneTasksOverdueNotification) Name() string { func (n *DailyTasksReminderNotification) Name() string { return "task.daily.reminder" }
return "task.undone.overdue"
}
// UserMentionedInTaskNotification represents a UserMentionedInTaskNotification notification // UserMentionedInTaskNotification represents a UserMentionedInTaskNotification notification
type UserMentionedInTaskNotification struct { type UserMentionedInTaskNotification struct {

View File

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

View File

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

View File

@ -158,7 +158,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
creators := []*userWithTask{} creators := []*userWithTask{}
err = s.Table("tasks"). 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"). Join("INNER", "users", "tasks.created_by_id = users.id").
Where(builder.And(conditions...)). Where(builder.And(conditions...)).
Find(&creators) Find(&creators)
@ -182,7 +182,7 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64, cond builder.Cond) (
assignees := []*TaskAssigneeWithUser{} assignees := []*TaskAssigneeWithUser{}
err = s.Table("task_assignees"). 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"). Join("INNER", "users", "task_assignees.user_id = users.id").
Where(builder.And(assigneeConds...)). Where(builder.And(assigneeConds...)).
Find(&assignees) Find(&assignees)

View File

@ -32,13 +32,13 @@ import (
"xorm.io/xorm" "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) now = utils.GetTimeWithoutSeconds(now)
nextMinute := now.Add(1 * time.Minute) nextMinute := now.Add(1 * time.Minute)
var tasks []*Task var tasks []*Task
err = s. err = s.
Where("due_date is not null AND due_date < ? 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"). Join("LEFT", "projects", "projects.id = tasks.project_id").
And("done = false"). And("done = false").
Find(&tasks) 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 // 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 { if err != nil {
return nil, err return nil, err
} }
overdueMailTime := time.Date(now.Year(), now.Month(), now.Day(), tm.Hour(), tm.Minute(), 0, 0, tz) reminderTime := time.Date(now.Year(), now.Month(), now.Day(), tm.Hour(), tm.Minute(), 0, 0, tz)
isTimeForReminder := overdueMailTime.After(now) || overdueMailTime.Equal(now.In(tz)) isTimeForReminder := reminderTime.After(now) || reminderTime.Equal(now.In(tz))
wasTimeForReminder := overdueMailTime.Before(nextMinute) wasTimeForReminder := reminderTime.Before(nextMinute)
taskIsOverdueInUserTimezone := overdueMailTime.After(t.Task.DueDate.In(tz)) taskDue := t.Task.DueDate.In(tz)
if isTimeForReminder && wasTimeForReminder && taskIsOverdueInUserTimezone { endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, tz)
if isTimeForReminder && wasTimeForReminder {
_, exists := uts[t.User.ID] _, exists := uts[t.User.ID]
if !exists { if !exists {
uts[t.User.ID] = &userWithTasks{ uts[t.User.ID] = &userWithTasks{
user: t.User, user: t.User,
tasks: make(map[int64]*Task), 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
}
} }
} }
@ -106,10 +115,11 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time, cond builder.Cond) (u
type userWithTasks struct { type userWithTasks struct {
user *user.User user *user.User
tasks map[int64]*Task 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() { func RegisterOverdueReminderCron() {
webhookEnabled := config.WebhooksEnabled.GetBool() webhookEnabled := config.WebhooksEnabled.GetBool()
emailEnabled := config.ServiceEnableEmailReminders.GetBool() && config.MailerEnabled.GetBool() emailEnabled := config.ServiceEnableEmailReminders.GetBool() && config.MailerEnabled.GetBool()
@ -130,12 +140,15 @@ func RegisterOverdueReminderCron() {
var cond builder.Cond var cond builder.Cond
if emailEnabled && !webhookEnabled { 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 { 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 return
} }
@ -143,80 +156,92 @@ func RegisterOverdueReminderCron() {
return 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{} taskIDs := []int64{}
for _, ut := range uts { 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) taskIDs = append(taskIDs, t.ID)
} }
} }
projects, err := GetProjectsMapSimpleByTaskIDs(s, taskIDs) projects, err := GetProjectsMapSimpleByTaskIDs(s, taskIDs)
if err != nil { 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 return
} }
for _, ut := range uts { // Dispatch webhook events, deduplicated by task ID across all users
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],
}
}
}
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 { if webhookEnabled {
// Per-task events dispatchedTasks := make(map[int64]bool)
for _, t := range ut.tasks { for _, ut := range uts {
// Per-task overdue events
for _, t := range ut.overdue {
if dispatchedTasks[t.ID] {
continue
}
dispatchedTasks[t.ID] = true
err = events.Dispatch(&TaskOverdueEvent{ err = events.Dispatch(&TaskOverdueEvent{
Task: t, Task: t,
User: ut.user, User: ut.user,
Project: projects[t.ProjectID], Project: projects[t.ProjectID],
}) })
if err != nil { 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 // Batch event
if len(ut.overdue) > 0 {
err = events.Dispatch(&TasksOverdueEvent{ err = events.Dispatch(&TasksOverdueEvent{
Tasks: mapToSlice(ut.tasks), Tasks: mapToSlice(ut.overdue),
User: ut.user, User: ut.user,
Projects: projects, Projects: projects,
}) })
if err != nil { if err != nil {
log.Errorf("[Undone Overdue Tasks Reminder] Could not dispatch batch overdue event for user %d: %s", ut.user.ID, err) 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 { 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 { 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" "xorm.io/builder"
) )
func TestGetUndoneOverDueTasks(t *testing.T) { func TestGetTasksForDailyReminder(t *testing.T) {
t.Run("no undone tasks", func(t *testing.T) { t.Run("no undone tasks", func(t *testing.T) {
db.LoadAndAssertFixtures(t) db.LoadAndAssertFixtures(t)
s := db.NewSession() s := db.NewSession()
@ -35,34 +35,61 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z") now, err := time.Parse(time.RFC3339Nano, "2018-01-01T01:13:00Z")
require.NoError(t, err) 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) require.NoError(t, err)
assert.Empty(t, tasks) 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) db.LoadAndAssertFixtures(t)
s := db.NewSession() s := db.NewSession()
defer s.Close() defer s.Close()
now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z") now, err := time.Parse(time.RFC3339Nano, "2018-12-01T09:00:00Z")
require.NoError(t, err) 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.NoError(t, err)
require.Len(t, uts, 1) assert.Len(t, uts, 1)
assert.Len(t, uts[1].tasks, 2) assert.Len(t, uts[1].overdue, 2)
// The tasks don't always have the same order, so we only check their presence, not their position. assert.Len(t, uts[1].dueToday, 1)
var task5Present bool _, ok := uts[1].dueToday[47]
var task6Present bool assert.True(t, ok)
for _, t := range uts[1].tasks {
if t.ID == 5 { // Disable today reminders and ensure the task is not included
task5Present = true _, err = s.Where("id = ?", 1).Cols("today_tasks_reminders_enabled").Update(&user.User{TodayTasksRemindersEnabled: false})
} require.NoError(t, err)
if t.ID == 6 { uts, err = getTasksForDailyReminder(s, now, builder.Or(
task6Present = true builder.Eq{"users.overdue_tasks_reminders_enabled": true},
} builder.Eq{"users.today_tasks_reminders_enabled": true},
} ))
assert.Truef(t, task5Present, "expected task 5 to be present but was not") require.NoError(t, err)
assert.Truef(t, task6Present, "expected task 6 to be present but was not") 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) { t.Run("done overdue", func(t *testing.T) {
db.LoadAndAssertFixtures(t) db.LoadAndAssertFixtures(t)
@ -71,7 +98,10 @@ func TestGetUndoneOverDueTasks(t *testing.T) {
now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z") now, err := time.Parse(time.RFC3339Nano, "2018-11-01T01:13:00Z")
require.NoError(t, err) 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) require.NoError(t, err)
assert.Empty(t, tasks) assert.Empty(t, tasks)
}) })

View File

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

View File

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

View File

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

View File

@ -105,7 +105,8 @@ type User struct {
DiscoverableByName bool `xorm:"bool default false index" json:"-"` DiscoverableByName bool `xorm:"bool default false index" json:"-"`
DiscoverableByEmail bool `xorm:"bool default false index" json:"-"` DiscoverableByEmail bool `xorm:"bool default false index" json:"-"`
OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"` OverdueTasksRemindersEnabled bool `xorm:"bool default true index" json:"-"`
OverdueTasksRemindersTime string `xorm:"varchar(5) not null default '09:00'" json:"-"` 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:"-"` DefaultProjectID int64 `xorm:"bigint null index" json:"-"`
WeekStart int `xorm:"null" json:"-"` WeekStart int `xorm:"null" json:"-"`
Language string `xorm:"varchar(50) null" json:"-" valid:"language"` 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 = "" userOut.Email = ""
} }
if userOut.OverdueTasksRemindersTime == "" { if userOut.TodayTasksRemindersTime == "" {
userOut.OverdueTasksRemindersTime = "9:00" userOut.TodayTasksRemindersTime = "9:00"
} }
if userOut.Status == StatusDisabled { if userOut.Status == StatusDisabled {
@ -629,11 +630,12 @@ func UpdateUser(s *xorm.Session, user *User, forceOverride bool) (updatedUser *U
"discoverable_by_name", "discoverable_by_name",
"discoverable_by_email", "discoverable_by_email",
"overdue_tasks_reminders_enabled", "overdue_tasks_reminders_enabled",
"today_tasks_reminders_enabled",
"default_project_id", "default_project_id",
"week_start", "week_start",
"language", "language",
"timezone", "timezone",
"overdue_tasks_reminders_time", "today_tasks_reminders_time",
"frontend_settings", "frontend_settings",
"extra_settings_links", "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.DiscoverableByName = config.DefaultSettingsDiscoverableByName.GetBool()
user.DiscoverableByEmail = config.DefaultSettingsDiscoverableByEmail.GetBool() user.DiscoverableByEmail = config.DefaultSettingsDiscoverableByEmail.GetBool()
user.OverdueTasksRemindersEnabled = config.DefaultSettingsOverdueTaskRemindersEnabled.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.DefaultProjectID = config.DefaultSettingsDefaultProjectID.GetInt64()
user.WeekStart = config.DefaultSettingsWeekStart.GetInt() user.WeekStart = config.DefaultSettingsWeekStart.GetInt()
user.Timezone = config.DefaultSettingsTimezone.GetString() user.Timezone = config.DefaultSettingsTimezone.GetString()

View File

@ -132,7 +132,7 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) { t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
require.NoError(t, err) 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) { t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams) 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) { t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
require.NoError(t, err) 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 // should equal duedate asc
t.Run("by due_date", func(t *testing.T) { 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) { 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) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
require.NoError(t, err) 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 // Due date without unix suffix
t.Run("by duedate asc without suffix", func(t *testing.T) { 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) { 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) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, urlParams)
require.NoError(t, err) 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) { 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) 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) { t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
require.NoError(t, err) 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) { t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil) 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) { t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
require.NoError(t, err) 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 // should equal duedate asc
t.Run("by due_date", func(t *testing.T) { 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) { 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) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date"}, "order_by": []string{"desc"}}, nil)
require.NoError(t, err) 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) { 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) 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), "CreateNewProjectForUser": reflect.ValueOf(models.CreateNewProjectForUser),
"CreateProject": reflect.ValueOf(models.CreateProject), "CreateProject": reflect.ValueOf(models.CreateProject),
"CreateSession": reflect.ValueOf(models.CreateSession), "CreateSession": reflect.ValueOf(models.CreateSession),
"DailyTasksReminderNotification": reflect.ValueOf((*models.DailyTasksReminderNotification)(nil)),
"DeleteAllUserSessions": reflect.ValueOf(models.DeleteAllUserSessions), "DeleteAllUserSessions": reflect.ValueOf(models.DeleteAllUserSessions),
"DeleteOrphanedTaskPositions": reflect.ValueOf(models.DeleteOrphanedTaskPositions), "DeleteOrphanedTaskPositions": reflect.ValueOf(models.DeleteOrphanedTaskPositions),
"DeleteUser": reflect.ValueOf(models.DeleteUser), "DeleteUser": reflect.ValueOf(models.DeleteUser),
@ -501,8 +502,6 @@ func init() {
"TeamProject": reflect.ValueOf((*models.TeamProject)(nil)), "TeamProject": reflect.ValueOf((*models.TeamProject)(nil)),
"TeamUser": reflect.ValueOf((*models.TeamUser)(nil)), "TeamUser": reflect.ValueOf((*models.TeamUser)(nil)),
"TeamWithPermission": reflect.ValueOf((*models.TeamWithPermission)(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)), "UnsplashPhoto": reflect.ValueOf((*models.UnsplashPhoto)(nil)),
"UpdateTaskInSavedFilterViews": reflect.ValueOf((*models.UpdateTaskInSavedFilterViews)(nil)), "UpdateTaskInSavedFilterViews": reflect.ValueOf((*models.UpdateTaskInSavedFilterViews)(nil)),
"UserDataExportRequestedEvent": reflect.ValueOf((*models.UserDataExportRequestedEvent)(nil)), "UserDataExportRequestedEvent": reflect.ValueOf((*models.UserDataExportRequestedEvent)(nil)),