Single frequency setting; per-user override; footer line
Admin-facing simplification:
- Dropped separate \"lookahead\" and \"historical lookahead\" tree
prefs (and the once-per-month historical gate). A single
\"send every N days\" number now drives both the cron cadence
and the window each issue looks ahead for living + deceased
events.
- Default 14, range 1–90, applies uniformly.
User-facing addition:
- The /my-account/{tree} subscription card gained an \"Email
frequency\" select with options: use site default, weekly,
every 2 weeks, monthly, every 2 months, quarterly. Stored as
a per-tree-per-user preference.
- Dispatch now checks each recipient's own cadence against
their own last-sent timestamp. Admin-added external addresses
with no webtrees account always receive every run (no
per-user state).
- Newsletter footer now reads \"You can change how often you
receive this email, or unsubscribe entirely, in the Newsletter
subscription section on your My account page\" — true now
that the control exists.
German translations updated for the new strings; stale ones
removed.
This commit is contained in:
@@ -86,18 +86,9 @@ final class NewsletterDispatchService
|
||||
continue;
|
||||
}
|
||||
|
||||
$due_at = Configuration::lastSentAt($tree)
|
||||
+ Configuration::frequencyDays($tree) * 86400;
|
||||
|
||||
if (!$force && $now < $due_at) {
|
||||
$log[] = sprintf(
|
||||
'Tree "%s": not due yet (next send in %d hours).',
|
||||
$tree->name(),
|
||||
(int) (($due_at - $now) / 3600),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Per-tree "is anyone due?" is decided inside dispatchForTree
|
||||
// — each recipient has their own cadence and last-sent
|
||||
// timestamp, so the gate is per-user, not per-tree.
|
||||
$log[] = $this->dispatchForTree($tree, $module, $now, $force);
|
||||
}
|
||||
|
||||
@@ -107,25 +98,18 @@ final class NewsletterDispatchService
|
||||
private function dispatchForTree(Tree $tree, ModuleInterface $module, int $now, bool $force): string
|
||||
{
|
||||
$include_anniversaries = Configuration::includeAnniversaries($tree);
|
||||
$lookahead = Configuration::lookaheadDays($tree);
|
||||
$historical_lookahead = Configuration::historicalLookaheadDays($tree);
|
||||
$current_month = date('Y-m', $now);
|
||||
|
||||
// Normally the historical section only appears on the first
|
||||
// scheduled send of each calendar month. Forced sends (admin
|
||||
// hitting "Send now" to preview the newsletter) always include
|
||||
// it — otherwise re-clicking the button silently strips the
|
||||
// section after the first run of the month.
|
||||
$include_historical = $force
|
||||
|| Configuration::lastHistoricalMonth($tree) !== $current_month;
|
||||
// One number controls everything: how often the newsletter is
|
||||
// sent AND how far ahead each issue looks for events. Same
|
||||
// window applies to living birthdays/anniversaries and to the
|
||||
// historical (deceased) section.
|
||||
$window = Configuration::frequencyDays($tree);
|
||||
|
||||
$birthdays = $this->event_query_service->upcomingBirthdays($tree, $lookahead);
|
||||
$birthdays = $this->event_query_service->upcomingBirthdays($tree, $window);
|
||||
$anniversaries = $include_anniversaries
|
||||
? $this->event_query_service->upcomingAnniversaries($tree, $lookahead)
|
||||
: null;
|
||||
$historical = $include_historical
|
||||
? $this->event_query_service->upcomingHistoricalEvents($tree, $historical_lookahead)
|
||||
? $this->event_query_service->upcomingAnniversaries($tree, $window)
|
||||
: null;
|
||||
$historical = $this->event_query_service->upcomingHistoricalEvents($tree, $window);
|
||||
|
||||
// Suppress entirely empty newsletters so subscribers don't get
|
||||
// a near-empty email on a slow fortnight.
|
||||
@@ -172,6 +156,10 @@ final class NewsletterDispatchService
|
||||
// labels are personalised relative to whichever individual
|
||||
// record the recipient is linked to in this tree.
|
||||
foreach ($group as $recipient) {
|
||||
if (!$this->recipientIsDue($tree, $recipient, $now, $force)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relationships = $this->relationshipMap($tree, $recipient, $featured);
|
||||
$detailed_set = $this->detailedXrefs($tree, $recipient, $featured);
|
||||
|
||||
@@ -189,9 +177,7 @@ final class NewsletterDispatchService
|
||||
'anniversaries' => $anniversaries,
|
||||
'historical' => $historical,
|
||||
'include_anniversaries' => $include_anniversaries,
|
||||
'include_historical' => $include_historical,
|
||||
'lookahead_days' => $lookahead,
|
||||
'historical_lookahead' => $historical_lookahead,
|
||||
'window_days' => $window,
|
||||
'generated_at' => $now,
|
||||
'avatar_cids' => $avatar_cids,
|
||||
'relationships' => $relationships,
|
||||
@@ -204,6 +190,12 @@ final class NewsletterDispatchService
|
||||
try {
|
||||
if ($this->mailer->sendWithEmbeds($from, $recipient, $from, $subject, $text, $html, $recipient_avatars)) {
|
||||
$sent++;
|
||||
// Per-user last-sent is stored only for real
|
||||
// webtrees users (id > 0). External admin-
|
||||
// added addresses always fire on every run.
|
||||
if ($recipient instanceof User) {
|
||||
Configuration::setUserLastSentAt($tree, $recipient, $now);
|
||||
}
|
||||
} else {
|
||||
$failures++;
|
||||
}
|
||||
@@ -219,20 +211,43 @@ final class NewsletterDispatchService
|
||||
I18N::init($original_locale);
|
||||
}
|
||||
|
||||
Configuration::setLastSentAt($tree, $now);
|
||||
if ($include_historical) {
|
||||
Configuration::setLastHistoricalMonth($tree, $current_month);
|
||||
// Tree-level last-sent is kept for the admin "Last sent" line
|
||||
// on the preferences page; it no longer gates dispatch.
|
||||
if ($sent > 0 || $failures > 0) {
|
||||
Configuration::setLastSentAt($tree, $now);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'Tree "%s": sent to %d recipient(s), %d failure(s)%s.',
|
||||
'Tree "%s": sent to %d recipient(s), %d failure(s).',
|
||||
$tree->name(),
|
||||
$sent,
|
||||
$failures,
|
||||
$include_historical ? ', monthly historical section included' : '',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the recipient's own cadence has elapsed since their
|
||||
* last newsletter (or if `$force` is set). External non-user
|
||||
* recipients have no per-user timestamp — they always fire so
|
||||
* admin-managed mailing lists still get an issue every run.
|
||||
*/
|
||||
private function recipientIsDue(Tree $tree, UserInterface $recipient, int $now, bool $force): bool
|
||||
{
|
||||
if ($force) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$recipient instanceof User) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$last = Configuration::userLastSentAt($tree, $recipient);
|
||||
$freq = Configuration::effectiveFrequencyDays($tree, $recipient);
|
||||
$due_at = $last + $freq * 86400;
|
||||
|
||||
return $now >= $due_at;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a "highlighted" image for every individual mentioned in
|
||||
* the newsletter and return a CID-keyed map of bytes + MIME type
|
||||
|
||||
Reference in New Issue
Block a user