mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Due-date reminders: honour hour 0 (midnight) and notify card assignees (#3192).
Two source bugs kept due-date reminder mail from reaching people. (1) The reminder hour was parsed as parseInt(NOTIFY_DUE_AT_HOUR_OF_DAY, 10) || 8, so a configured hour of 0 (midnight) silently became 8 (0 is falsy) and out-of-range hours were accepted; parseNotifyDueHour now keeps 0 and rejects anything outside 0..23. (2) A due-card activity's notification participants were only the card CREATOR and its MEMBERS (server/models/activities.js) -- a user ASSIGNED a card but not a member was never notified about it, including its due date; card.assignees are now included too (still gated downstream by the board tracking level, so nobody who is not tracking is newly emailed). The NOTIFY_DUE_* parsing is extracted to a pure, unit-tested models/lib/dueNotificationConfig.js. Note: the built-in reminder still requires NOTIFY_DUE_DAYS_BEFORE_AND_AFTER to be set, and members/assignees are emailed per their board watch/tracking level. Tests: tests/dueNotificationConfig.test.cjs (12 checks) covers the midnight-hour case, valid/invalid/out-of-range hours, the day-offset window parsing and negatives, plus source guards that cards.js uses parseNotifyDueHour and that assignees are in the notification participants. Thanks to SirConfigMgr and xet7 ! Fixes #3192,
This commit is contained in:
1 parent
8f2189a66f
commit
c0bbacaf94
4 files changed
+136
-15
No files matched your search
+8
-14
@@ -3468,24 +3468,18 @@ const findDueCards = async days => {
|
||||
};
|
||||
const addCronJob = debounce(
|
||||
function findDueCardsDebounced() {
|
||||
const { parseNotifyDueDays, parseNotifyDueHour } = require('/models/lib/dueNotificationConfig');
|
||||
const envValue = process.env.NOTIFY_DUE_DAYS_BEFORE_AND_AFTER;
|
||||
if (!envValue) {
|
||||
return;
|
||||
}
|
||||
const notifydays = envValue
|
||||
.split(',')
|
||||
.map(value => {
|
||||
const iValue = parseInt(value, 10);
|
||||
// Allow -14..14: positive = days before due, 0 = due today, negative = days past due
|
||||
if (isNaN(iValue) || iValue < -14 || iValue > 14) {
|
||||
return false;
|
||||
}
|
||||
return iValue;
|
||||
})
|
||||
.filter(v => v !== false);
|
||||
const notifyitvl = process.env.NOTIFY_DUE_AT_HOUR_OF_DAY; //passed in the itvl has to be a number standing for the hour of current time
|
||||
const defaultitvl = 8; // default every morning at 8am, if the passed env variable has parsing error use default
|
||||
const itvl = parseInt(notifyitvl, 10) || defaultitvl;
|
||||
// -14..14: positive = days before due, 0 = due today, negative = days past due.
|
||||
const notifydays = parseNotifyDueDays(envValue);
|
||||
const defaultitvl = 8; // default every morning at 8am if the env var is missing/invalid
|
||||
// #3192: parseNotifyDueHour keeps a configured hour of 0 (midnight) instead of
|
||||
// the old `parseInt(..) || 8` that turned the falsy 0 into 8, and rejects
|
||||
// out-of-range hours.
|
||||
const itvl = parseNotifyDueHour(process.env.NOTIFY_DUE_AT_HOUR_OF_DAY, defaultitvl);
|
||||
const scheduler = (job => () => {
|
||||
const now = new Date();
|
||||
const hour = 3600 * 1e3;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
// Pure parsers for the due-date reminder configuration (NOTIFY_DUE_* env vars),
|
||||
// extracted from models/cards.js so they are unit-testable without Meteor.
|
||||
// Related to #3192 (due-date reminder mail).
|
||||
|
||||
// Parse NOTIFY_DUE_DAYS_BEFORE_AND_AFTER: a comma-separated list of day offsets
|
||||
// (positive = days before due, 0 = due today, negative = days past due). Each is
|
||||
// validated to the -14..14 window; invalid/out-of-range entries are dropped.
|
||||
// Returns an array of integers (possibly empty).
|
||||
function parseNotifyDueDays(envValue) {
|
||||
if (!envValue || typeof envValue !== 'string') return [];
|
||||
return envValue
|
||||
.split(',')
|
||||
.map(value => {
|
||||
const iValue = parseInt(value, 10);
|
||||
if (Number.isNaN(iValue) || iValue < -14 || iValue > 14) return false;
|
||||
return iValue;
|
||||
})
|
||||
.filter(v => v !== false);
|
||||
}
|
||||
|
||||
// Parse NOTIFY_DUE_AT_HOUR_OF_DAY: the hour of day (0..23) at which to send the
|
||||
// reminders. Returns the parsed hour, or `defaultHour` when the value is missing,
|
||||
// non-numeric, or out of range.
|
||||
//
|
||||
// #3192: the previous `parseInt(env, 10) || defaultHour` silently turned a valid
|
||||
// hour of 0 (midnight) into the default (8), because 0 is falsy — so midnight
|
||||
// reminders were impossible. It also accepted out-of-range hours (e.g. 25, -1).
|
||||
// Number.isNaN + a 0..23 range check fixes both while keeping the "parse error ->
|
||||
// default" behaviour.
|
||||
function parseNotifyDueHour(envValue, defaultHour = 8) {
|
||||
const hour = parseInt(envValue, 10);
|
||||
if (Number.isNaN(hour) || hour < 0 || hour > 23) return defaultHour;
|
||||
return hour;
|
||||
}
|
||||
|
||||
module.exports = { parseNotifyDueDays, parseNotifyDueHour };
|
||||
@@ -139,7 +139,16 @@ Activities.after.insert(async (userId, doc) => {
|
||||
if (activity.cardId) {
|
||||
const card = (await activity.card()) || (await Cards.findOneAsync(activity.cardId));
|
||||
if (card) {
|
||||
participants = [...new Set([...participants, card.userId, ...(card.members || [])])];
|
||||
// #3192: include the card's ASSIGNEES as participants too, not just its
|
||||
// creator and members — a user assigned a card (e.g. one with a due date)
|
||||
// must be notified about it. Participants are still gated downstream by the
|
||||
// board tracking level, so this does not notify anyone who is not tracking.
|
||||
participants = [...new Set([
|
||||
...participants,
|
||||
card.userId,
|
||||
...(card.members || []),
|
||||
...(card.assignees || []),
|
||||
])];
|
||||
watchers = [...new Set([...watchers, ...(card.watchers || [])])];
|
||||
params.card = normalizeActivityText(card.title);
|
||||
title = 'act-withCardTitle';
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
// Unit + negative tests for the due-date reminder config parsers (#3192).
|
||||
// Run: node tests/dueNotificationConfig.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {
|
||||
parseNotifyDueDays,
|
||||
parseNotifyDueHour,
|
||||
} = require('../models/lib/dueNotificationConfig');
|
||||
|
||||
let passed = 0;
|
||||
function check(name, fn) { fn(); passed += 1; console.log(' ok -', name); }
|
||||
|
||||
// ── parseNotifyDueHour — the #3192 midnight bug ─────────────────────────────
|
||||
check('#3192: hour "0" (midnight) is kept, NOT silently turned into the default', () => {
|
||||
// The old `parseInt(env,10) || 8` returned 8 for "0" because 0 is falsy.
|
||||
assert.strictEqual(parseNotifyDueHour('0', 8), 0);
|
||||
});
|
||||
check('a valid hour is parsed as itself', () => {
|
||||
assert.strictEqual(parseNotifyDueHour('8', 8), 8);
|
||||
assert.strictEqual(parseNotifyDueHour('23', 8), 23);
|
||||
assert.strictEqual(parseNotifyDueHour('13', 8), 13);
|
||||
});
|
||||
check('missing / non-numeric hour falls back to the default', () => {
|
||||
assert.strictEqual(parseNotifyDueHour(undefined, 8), 8);
|
||||
assert.strictEqual(parseNotifyDueHour('', 8), 8);
|
||||
assert.strictEqual(parseNotifyDueHour('abc', 8), 8);
|
||||
});
|
||||
check('out-of-range hour falls back to the default (negative, >23)', () => {
|
||||
assert.strictEqual(parseNotifyDueHour('-1', 8), 8);
|
||||
assert.strictEqual(parseNotifyDueHour('24', 8), 8);
|
||||
assert.strictEqual(parseNotifyDueHour('25', 8), 8);
|
||||
});
|
||||
check('the default is configurable', () => {
|
||||
assert.strictEqual(parseNotifyDueHour('nope', 6), 6);
|
||||
});
|
||||
|
||||
// ── parseNotifyDueDays ──────────────────────────────────────────────────────
|
||||
check('parses a comma list of day offsets, keeping 0 and negatives', () => {
|
||||
assert.deepStrictEqual(parseNotifyDueDays('-2,0,3'), [-2, 0, 3]);
|
||||
});
|
||||
check('drops entries outside the -14..14 window', () => {
|
||||
assert.deepStrictEqual(parseNotifyDueDays('-15,-14,14,15,100'), [-14, 14]);
|
||||
});
|
||||
check('drops non-numeric entries but keeps the valid ones', () => {
|
||||
assert.deepStrictEqual(parseNotifyDueDays('0,foo,2'), [0, 2]);
|
||||
});
|
||||
check('empty / undefined / non-string yields an empty array (negative)', () => {
|
||||
assert.deepStrictEqual(parseNotifyDueDays(''), []);
|
||||
assert.deepStrictEqual(parseNotifyDueDays(undefined), []);
|
||||
assert.deepStrictEqual(parseNotifyDueDays(null), []);
|
||||
assert.deepStrictEqual(parseNotifyDueDays(5), []);
|
||||
});
|
||||
check('a single valid value works', () => {
|
||||
assert.deepStrictEqual(parseNotifyDueDays('0'), [0]);
|
||||
});
|
||||
|
||||
// ── source guards ───────────────────────────────────────────────────────────
|
||||
const read = rel => fs.readFileSync(path.join(__dirname, '..', rel), 'utf8');
|
||||
|
||||
check('cards.js uses parseNotifyDueHour (no falsy `|| defaultitvl` hour parse)', () => {
|
||||
const src = read('models/cards.js');
|
||||
assert.ok(/parseNotifyDueHour\(process\.env\.NOTIFY_DUE_AT_HOUR_OF_DAY/.test(src),
|
||||
'the scheduler must parse the hour via parseNotifyDueHour');
|
||||
assert.ok(!/parseInt\(notifyitvl, 10\) \|\| defaultitvl/.test(src),
|
||||
'the old falsy-zero hour parse must be gone');
|
||||
});
|
||||
check('#3192: due-card notifications include card ASSIGNEES as participants', () => {
|
||||
const src = read('server/models/activities.js');
|
||||
// The card-activity participants block must add card.assignees alongside members.
|
||||
const m = src.match(/participants = \[\.\.\.new Set\(\[[\s\S]{0,200}?card\.userId[\s\S]{0,200}?\]\)\]/);
|
||||
assert.ok(m, 'the card participants assignment must exist');
|
||||
assert.ok(/card\.assignees/.test(m[0]),
|
||||
'participants must include card.assignees so assignees are notified');
|
||||
});
|
||||
|
||||
console.log(`\ndueNotificationConfig: ${passed} checks passed`);
|
||||
Reference in new issue
Block a user