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:
@@ -42,8 +42,6 @@ use Illuminate\Support\Collection;
|
|||||||
$id = $tree->id();
|
$id = $tree->id();
|
||||||
$enabled = Configuration::isEnabled($tree);
|
$enabled = Configuration::isEnabled($tree);
|
||||||
$frequency = Configuration::frequencyDays($tree);
|
$frequency = Configuration::frequencyDays($tree);
|
||||||
$lookahead = Configuration::lookaheadDays($tree);
|
|
||||||
$histLook = Configuration::historicalLookaheadDays($tree);
|
|
||||||
$annivs = Configuration::includeAnniversaries($tree);
|
$annivs = Configuration::includeAnniversaries($tree);
|
||||||
$subject = Configuration::subjectPrefix($tree);
|
$subject = Configuration::subjectPrefix($tree);
|
||||||
$extras = $tree->getPreference(Configuration::PREF_EXTRA_RECIPIENTS, '');
|
$extras = $tree->getPreference(Configuration::PREF_EXTRA_RECIPIENTS, '');
|
||||||
@@ -81,22 +79,9 @@ use Illuminate\Support\Collection;
|
|||||||
max="<?= Configuration::MAX_FREQUENCY_DAYS ?>" required>
|
max="<?= Configuration::MAX_FREQUENCY_DAYS ?>" required>
|
||||||
<span class="input-group-text"><?= I18N::translate('days') ?></span>
|
<span class="input-group-text"><?= I18N::translate('days') ?></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<small class="form-text text-muted">
|
||||||
</div>
|
<?= I18N::translate('Each issue looks the same number of days ahead, for both living relatives and historical events of those who have passed away. Default 14.') ?>
|
||||||
|
</small>
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label" for="lookahead-<?= $id ?>">
|
|
||||||
<?= I18N::translate('Look ahead') ?>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<div class="input-group" style="max-width: 18rem;">
|
|
||||||
<input class="form-control" type="number"
|
|
||||||
id="lookahead-<?= $id ?>" name="lookahead-<?= $id ?>"
|
|
||||||
value="<?= e((string) $lookahead) ?>"
|
|
||||||
min="<?= Configuration::MIN_LOOKAHEAD_DAYS ?>"
|
|
||||||
max="<?= Configuration::MAX_LOOKAHEAD_DAYS ?>" required>
|
|
||||||
<span class="input-group-text"><?= I18N::translate('days') ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -116,20 +101,6 @@ use Illuminate\Support\Collection;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label" for="historical-<?= $id ?>">
|
|
||||||
<?= I18N::translate('Historical look-ahead (days)') ?>
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<input class="form-control" type="number" style="max-width: 18rem;"
|
|
||||||
id="historical-<?= $id ?>" name="historical-<?= $id ?>"
|
|
||||||
value="<?= e((string) $histLook) ?>" min="7" max="60" required>
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
<?= I18N::translate('Births and deaths of deceased people are included once per calendar month.') ?>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<label class="col-sm-3 col-form-label" for="lineal-<?= $id ?>">
|
<label class="col-sm-3 col-form-label" for="lineal-<?= $id ?>">
|
||||||
<?= I18N::translate('Detailed view distance') ?>
|
<?= I18N::translate('Detailed view distance') ?>
|
||||||
|
|||||||
@@ -181,6 +181,32 @@ use Fisharebest\Webtrees\Tree;
|
|||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<?= I18N::translate('You will receive a periodic email with upcoming birthdays and other family events from %s.', e($tree->title())) ?>
|
<?= I18N::translate('You will receive a periodic email with upcoming birthdays and other family events from %s.', e($tree->title())) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$current_freq = Configuration::userFrequencyDays($tree, $user);
|
||||||
|
$tree_freq = Configuration::frequencyDays($tree);
|
||||||
|
$freq_labels = [
|
||||||
|
0 => I18N::translate('Use site default (every %d days)', $tree_freq),
|
||||||
|
7 => I18N::translate('Weekly'),
|
||||||
|
14 => I18N::translate('Every 2 weeks'),
|
||||||
|
30 => I18N::translate('Monthly'),
|
||||||
|
60 => I18N::translate('Every 2 months'),
|
||||||
|
90 => I18N::translate('Quarterly'),
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="newsletter_frequency" class="form-label">
|
||||||
|
<?= I18N::translate('Email frequency') ?>
|
||||||
|
</label>
|
||||||
|
<select class="form-select" id="newsletter_frequency"
|
||||||
|
name="newsletter_frequency" style="max-width: 22rem;">
|
||||||
|
<?php foreach ($freq_labels as $days => $label) : ?>
|
||||||
|
<option value="<?= $days ?>" <?= $days === $current_freq ? 'selected' : '' ?>>
|
||||||
|
<?= e($label) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ use Illuminate\Support\Collection;
|
|||||||
* @var Collection<int,Fact>|null $anniversaries
|
* @var Collection<int,Fact>|null $anniversaries
|
||||||
* @var Collection<int,Fact>|null $historical
|
* @var Collection<int,Fact>|null $historical
|
||||||
* @var bool $include_anniversaries
|
* @var bool $include_anniversaries
|
||||||
* @var bool $include_historical
|
* @var int $window_days Shared lookahead window for living + deceased events
|
||||||
* @var int $lookahead_days
|
|
||||||
* @var int $historical_lookahead
|
|
||||||
* @var int $generated_at
|
* @var int $generated_at
|
||||||
* @var array<string,string> $avatar_cids xref => CID name
|
* @var array<string,string> $avatar_cids xref => CID name
|
||||||
* @var array<string,string> $relationships xref => "your mother" etc. (per-recipient)
|
* @var array<string,string> $relationships xref => "your mother" etc. (per-recipient)
|
||||||
@@ -597,7 +595,7 @@ $timeline_arrow_row = '<tr>'
|
|||||||
<div style="font-size:13px;font-weight:300;color:<?= $palette['ink3'] ?>;">
|
<div style="font-size:13px;font-weight:300;color:<?= $palette['ink3'] ?>;">
|
||||||
<?= e($masthead_date($generated_at)) ?>
|
<?= e($masthead_date($generated_at)) ?>
|
||||||
<span style="color:<?= $palette['mute'] ?>;">·</span>
|
<span style="color:<?= $palette['mute'] ?>;">·</span>
|
||||||
<?= e(I18N::translate('Events in the next %d days.', $lookahead_days)) ?>
|
<?= e(I18N::translate('Events in the next %d days.', $window_days)) ?>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -706,7 +704,7 @@ $timeline_arrow_row = '<tr>'
|
|||||||
</td></tr>
|
</td></tr>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|
||||||
<?php if ($include_historical && $historical !== null && !$historical->isEmpty()) : ?>
|
<?php if ($historical !== null && !$historical->isEmpty()) : ?>
|
||||||
<?php
|
<?php
|
||||||
$detailed = [];
|
$detailed = [];
|
||||||
$summary = [];
|
$summary = [];
|
||||||
@@ -717,7 +715,7 @@ $timeline_arrow_row = '<tr>'
|
|||||||
<tr><td style="padding:32px 0 0;">
|
<tr><td style="padding:32px 0 0;">
|
||||||
<h2 style="<?= $section_title_style ?>"><?= e(I18N::translate('On this month in history')) ?></h2>
|
<h2 style="<?= $section_title_style ?>"><?= e(I18N::translate('On this month in history')) ?></h2>
|
||||||
<p style="<?= $section_kicker_style ?>">
|
<p style="<?= $section_kicker_style ?>">
|
||||||
<?= e(I18N::translate('Events in the next %d days for people who have passed away.', $historical_lookahead)) ?>
|
<?= e(I18N::translate('Events in the next %d days for people who have passed away.', $window_days)) ?>
|
||||||
</p>
|
</p>
|
||||||
<?php if ($detailed !== []) : ?>
|
<?php if ($detailed !== []) : ?>
|
||||||
<?= $card_open ?>
|
<?= $card_open ?>
|
||||||
@@ -764,7 +762,7 @@ $timeline_arrow_row = '<tr>'
|
|||||||
<?= e(I18N::translate('You are receiving this email because you subscribed to the %s newsletter.', $tree->title())) ?>
|
<?= e(I18N::translate('You are receiving this email because you subscribed to the %s newsletter.', $tree->title())) ?>
|
||||||
<br>
|
<br>
|
||||||
<?= I18N::translate(
|
<?= I18N::translate(
|
||||||
'To change or cancel your subscription, edit the “Newsletter subscription” section on your %s page.',
|
'You can change how often you receive this email, or unsubscribe entirely, in the “Newsletter subscription” section on your %s page.',
|
||||||
'<a href="' . e($account_url) . '" style="color:' . $palette['link'] . ';text-decoration:none;border-bottom:1px solid ' . $palette['link'] . ';">' . e(I18N::translate('My account')) . '</a>',
|
'<a href="' . e($account_url) . '" style="color:' . $palette['link'] . ';text-decoration:none;border-bottom:1px solid ' . $palette['link'] . ';">' . e(I18N::translate('My account')) . '</a>',
|
||||||
) ?>
|
) ?>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+52
-33
@@ -10,8 +10,8 @@ use Fisharebest\Webtrees\Tree;
|
|||||||
* Per-tree configuration for the Email Newsletter module.
|
* Per-tree configuration for the Email Newsletter module.
|
||||||
*
|
*
|
||||||
* All values are persisted via $tree->setPreference() / getPreference().
|
* All values are persisted via $tree->setPreference() / getPreference().
|
||||||
* State values (last-sent timestamp, last historical-section month) are
|
* State values (last-sent timestamp) are also stored on the tree because
|
||||||
* also stored on the tree because each tree produces its own newsletter.
|
* each tree produces its own newsletter on its own cadence.
|
||||||
*/
|
*/
|
||||||
final class Configuration
|
final class Configuration
|
||||||
{
|
{
|
||||||
@@ -19,29 +19,25 @@ final class Configuration
|
|||||||
// column. Keys here MUST stay <= 32 characters.
|
// column. Keys here MUST stay <= 32 characters.
|
||||||
public const string PREF_ENABLED = 'NEWSLETTER_ENABLED';
|
public const string PREF_ENABLED = 'NEWSLETTER_ENABLED';
|
||||||
public const string PREF_FREQUENCY_DAYS = 'NEWSLETTER_FREQ_DAYS';
|
public const string PREF_FREQUENCY_DAYS = 'NEWSLETTER_FREQ_DAYS';
|
||||||
public const string PREF_LOOKAHEAD_DAYS = 'NEWSLETTER_LOOK_DAYS';
|
|
||||||
public const string PREF_INCLUDE_ANNIVERSARIES = 'NEWSLETTER_INC_ANNIVS';
|
public const string PREF_INCLUDE_ANNIVERSARIES = 'NEWSLETTER_INC_ANNIVS';
|
||||||
public const string PREF_HISTORICAL_LOOKAHEAD = 'NEWSLETTER_HIST_DAYS';
|
|
||||||
public const string PREF_EXTRA_RECIPIENTS = 'NEWSLETTER_EXTRAS';
|
public const string PREF_EXTRA_RECIPIENTS = 'NEWSLETTER_EXTRAS';
|
||||||
public const string PREF_LINEAL_DEPTH = 'NEWSLETTER_LINEAL_DEPTH';
|
public const string PREF_LINEAL_DEPTH = 'NEWSLETTER_LINEAL_DEPTH';
|
||||||
public const string PREF_LAST_SENT_AT = 'NEWSLETTER_LAST_SENT';
|
public const string PREF_LAST_SENT_AT = 'NEWSLETTER_LAST_SENT';
|
||||||
public const string PREF_LAST_HISTORICAL_MONTH = 'NEWSLETTER_LAST_HIST_MO';
|
|
||||||
public const string PREF_SUBJECT_PREFIX = 'NEWSLETTER_SUBJ_PFX';
|
public const string PREF_SUBJECT_PREFIX = 'NEWSLETTER_SUBJ_PFX';
|
||||||
|
|
||||||
// Module-level (not tree-bound) settings.
|
// Module-level (not tree-bound) settings.
|
||||||
public const string MODULE_PREF_CRON_TOKEN = 'cron_token';
|
public const string MODULE_PREF_CRON_TOKEN = 'cron_token';
|
||||||
|
|
||||||
// Per-user subscription preference. Set via $user->setPreference().
|
// Per-tree-per-user preferences. Stored via
|
||||||
|
// $tree->setUserPreference($user, ...).
|
||||||
public const string USER_PREF_SUBSCRIBED = 'newsletter_subscribed';
|
public const string USER_PREF_SUBSCRIBED = 'newsletter_subscribed';
|
||||||
|
public const string USER_PREF_FREQUENCY_DAYS = 'NEWSLETTER_USER_FREQ_DAYS';
|
||||||
|
public const string USER_PREF_LAST_SENT_AT = 'NEWSLETTER_USER_LAST_SENT';
|
||||||
|
|
||||||
public const int DEFAULT_FREQUENCY_DAYS = 14;
|
public const int DEFAULT_FREQUENCY_DAYS = 14;
|
||||||
public const int DEFAULT_LOOKAHEAD_DAYS = 14;
|
|
||||||
public const int DEFAULT_HISTORICAL_LOOKAHEAD = 30;
|
|
||||||
public const int DEFAULT_LINEAL_DEPTH = 3;
|
public const int DEFAULT_LINEAL_DEPTH = 3;
|
||||||
public const int MIN_FREQUENCY_DAYS = 1;
|
public const int MIN_FREQUENCY_DAYS = 1;
|
||||||
public const int MAX_FREQUENCY_DAYS = 90;
|
public const int MAX_FREQUENCY_DAYS = 90;
|
||||||
public const int MIN_LOOKAHEAD_DAYS = 1;
|
|
||||||
public const int MAX_LOOKAHEAD_DAYS = 60;
|
|
||||||
public const int MIN_LINEAL_DEPTH = 0;
|
public const int MIN_LINEAL_DEPTH = 0;
|
||||||
public const int MAX_LINEAL_DEPTH = 10;
|
public const int MAX_LINEAL_DEPTH = 10;
|
||||||
|
|
||||||
@@ -50,6 +46,12 @@ final class Configuration
|
|||||||
return $tree->getPreference(self::PREF_ENABLED) === '1';
|
return $tree->getPreference(self::PREF_ENABLED) === '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The single cadence number: newsletters are sent every N days
|
||||||
|
* and each issue looks N days ahead for events. Used as both the
|
||||||
|
* send-interval and the look-ahead window for living and
|
||||||
|
* deceased relatives alike.
|
||||||
|
*/
|
||||||
public static function frequencyDays(Tree $tree): int
|
public static function frequencyDays(Tree $tree): int
|
||||||
{
|
{
|
||||||
$value = (int) $tree->getPreference(self::PREF_FREQUENCY_DAYS, (string) self::DEFAULT_FREQUENCY_DAYS);
|
$value = (int) $tree->getPreference(self::PREF_FREQUENCY_DAYS, (string) self::DEFAULT_FREQUENCY_DAYS);
|
||||||
@@ -57,13 +59,6 @@ final class Configuration
|
|||||||
return max(self::MIN_FREQUENCY_DAYS, min(self::MAX_FREQUENCY_DAYS, $value));
|
return max(self::MIN_FREQUENCY_DAYS, min(self::MAX_FREQUENCY_DAYS, $value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function lookaheadDays(Tree $tree): int
|
|
||||||
{
|
|
||||||
$value = (int) $tree->getPreference(self::PREF_LOOKAHEAD_DAYS, (string) self::DEFAULT_LOOKAHEAD_DAYS);
|
|
||||||
|
|
||||||
return max(self::MIN_LOOKAHEAD_DAYS, min(self::MAX_LOOKAHEAD_DAYS, $value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function includeAnniversaries(Tree $tree): bool
|
public static function includeAnniversaries(Tree $tree): bool
|
||||||
{
|
{
|
||||||
return $tree->getPreference(self::PREF_INCLUDE_ANNIVERSARIES, '1') === '1';
|
return $tree->getPreference(self::PREF_INCLUDE_ANNIVERSARIES, '1') === '1';
|
||||||
@@ -76,14 +71,48 @@ final class Configuration
|
|||||||
return max(self::MIN_LINEAL_DEPTH, min(self::MAX_LINEAL_DEPTH, $value));
|
return max(self::MIN_LINEAL_DEPTH, min(self::MAX_LINEAL_DEPTH, $value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function historicalLookaheadDays(Tree $tree): int
|
/**
|
||||||
|
* The set of user-selectable cadences, in days. 0 is a sentinel
|
||||||
|
* meaning "fall back to the tree's frequency". Other values are
|
||||||
|
* weekly / biweekly / monthly / bimonthly / quarterly.
|
||||||
|
*
|
||||||
|
* @return array<int,int>
|
||||||
|
*/
|
||||||
|
public static function userFrequencyOptions(): array
|
||||||
{
|
{
|
||||||
$value = (int) $tree->getPreference(
|
return [0, 7, 14, 30, 60, 90];
|
||||||
self::PREF_HISTORICAL_LOOKAHEAD,
|
}
|
||||||
(string) self::DEFAULT_HISTORICAL_LOOKAHEAD,
|
|
||||||
);
|
|
||||||
|
|
||||||
return max(7, min(60, $value));
|
public static function userFrequencyDays(Tree $tree, \Fisharebest\Webtrees\Contracts\UserInterface $user): int
|
||||||
|
{
|
||||||
|
$raw = (int) $tree->getUserPreference($user, self::USER_PREF_FREQUENCY_DAYS, '0');
|
||||||
|
|
||||||
|
if (!in_array($raw, self::userFrequencyOptions(), true)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolved cadence for a recipient: the user's choice if they
|
||||||
|
* picked one, otherwise the tree default.
|
||||||
|
*/
|
||||||
|
public static function effectiveFrequencyDays(Tree $tree, \Fisharebest\Webtrees\Contracts\UserInterface $user): int
|
||||||
|
{
|
||||||
|
$user_pref = self::userFrequencyDays($tree, $user);
|
||||||
|
|
||||||
|
return $user_pref > 0 ? $user_pref : self::frequencyDays($tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function userLastSentAt(Tree $tree, \Fisharebest\Webtrees\Contracts\UserInterface $user): int
|
||||||
|
{
|
||||||
|
return (int) $tree->getUserPreference($user, self::USER_PREF_LAST_SENT_AT, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function setUserLastSentAt(Tree $tree, \Fisharebest\Webtrees\Contracts\UserInterface $user, int $timestamp): void
|
||||||
|
{
|
||||||
|
$tree->setUserPreference($user, self::USER_PREF_LAST_SENT_AT, (string) $timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function subjectPrefix(Tree $tree): string
|
public static function subjectPrefix(Tree $tree): string
|
||||||
@@ -114,14 +143,4 @@ final class Configuration
|
|||||||
{
|
{
|
||||||
$tree->setPreference(self::PREF_LAST_SENT_AT, (string) $timestamp);
|
$tree->setPreference(self::PREF_LAST_SENT_AT, (string) $timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function lastHistoricalMonth(Tree $tree): string
|
|
||||||
{
|
|
||||||
return $tree->getPreference(self::PREF_LAST_HISTORICAL_MONTH, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function setLastHistoricalMonth(Tree $tree, string $yearMonth): void
|
|
||||||
{
|
|
||||||
$tree->setPreference(self::PREF_LAST_HISTORICAL_MONTH, $yearMonth);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ final class AccountUpdateDecorator implements RequestHandlerInterface
|
|||||||
Configuration::USER_PREF_SUBSCRIBED,
|
Configuration::USER_PREF_SUBSCRIBED,
|
||||||
$subscribed ? '1' : '0',
|
$subscribed ? '1' : '0',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Newsletter frequency: only persist a value the user
|
||||||
|
// actually picked from the offered options (0 = "use site
|
||||||
|
// default"). Anything else is silently dropped so a
|
||||||
|
// malformed POST can't pin them on an arbitrary cadence.
|
||||||
|
$frequency = Validator::parsedBody($request)
|
||||||
|
->isInArray(Configuration::userFrequencyOptions())
|
||||||
|
->integer('newsletter_frequency', 0);
|
||||||
|
|
||||||
|
$tree->setUserPreference(
|
||||||
|
$user,
|
||||||
|
Configuration::USER_PREF_FREQUENCY_DAYS,
|
||||||
|
(string) $frequency,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
|||||||
+11
-14
@@ -108,9 +108,9 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
'Subscribe to the newsletter' => 'Newsletter abonnieren',
|
'Subscribe to the newsletter' => 'Newsletter abonnieren',
|
||||||
'Send newsletters every' => 'Newsletter senden alle',
|
'Send newsletters every' => 'Newsletter senden alle',
|
||||||
'days' => 'Tage',
|
'days' => 'Tage',
|
||||||
'Look ahead' => 'Vorschau',
|
'Each issue looks the same number of days ahead, for both living relatives and historical events of those who have passed away. Default 14.'
|
||||||
|
=> 'Jede Ausgabe blickt um die gleiche Anzahl Tage in die Zukunft, sowohl für lebende Verwandte als auch für historische Ereignisse bereits verstorbener Personen. Standardwert 14.',
|
||||||
'Include marriage anniversaries' => 'Hochzeitstage einbeziehen',
|
'Include marriage anniversaries' => 'Hochzeitstage einbeziehen',
|
||||||
'Historical look-ahead (days)' => 'Historische Vorschau (Tage)',
|
|
||||||
'Extra recipient email addresses (one per line)' => 'Zusätzliche Empfänger-E-Mail-Adressen (eine pro Zeile)',
|
'Extra recipient email addresses (one per line)' => 'Zusätzliche Empfänger-E-Mail-Adressen (eine pro Zeile)',
|
||||||
'Subject prefix' => 'Betreff-Präfix',
|
'Subject prefix' => 'Betreff-Präfix',
|
||||||
'Save' => 'Speichern',
|
'Save' => 'Speichern',
|
||||||
@@ -141,15 +141,20 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
=> 'Sie erhalten regelmäßig eine E-Mail mit anstehenden Geburtstagen und weiteren Familienereignissen aus %s.',
|
=> 'Sie erhalten regelmäßig eine E-Mail mit anstehenden Geburtstagen und weiteren Familienereignissen aus %s.',
|
||||||
'You are receiving this email because you subscribed to the %s newsletter.'
|
'You are receiving this email because you subscribed to the %s newsletter.'
|
||||||
=> 'Sie erhalten diese E-Mail, weil Sie den Newsletter „%s“ abonniert haben.',
|
=> 'Sie erhalten diese E-Mail, weil Sie den Newsletter „%s“ abonniert haben.',
|
||||||
'To change or cancel your subscription, edit the “Newsletter subscription” section on your %s page.'
|
'You can change how often you receive this email, or unsubscribe entirely, in the “Newsletter subscription” section on your %s page.'
|
||||||
=> 'Um Ihr Abonnement zu ändern oder zu kündigen, bearbeiten Sie den Abschnitt „Newsletter-Abonnement“ auf Ihrer Seite %s.',
|
=> 'Wie oft Sie diese E-Mail erhalten – oder ob Sie sie ganz abbestellen möchten – können Sie im Abschnitt „Newsletter-Abonnement“ auf Ihrer Seite %s ändern.',
|
||||||
|
'Email frequency' => 'E-Mail-Häufigkeit',
|
||||||
|
'Use site default (every %d days)' => 'Standard der Seite verwenden (alle %d Tage)',
|
||||||
|
'Weekly' => 'Wöchentlich',
|
||||||
|
'Every 2 weeks' => 'Alle 2 Wochen',
|
||||||
|
'Monthly' => 'Monatlich',
|
||||||
|
'Every 2 months' => 'Alle 2 Monate',
|
||||||
|
'Quarterly' => 'Vierteljährlich',
|
||||||
'Configure newsletter dispatch on a per-tree basis. The sender is the contact user of each tree (falling back to the site webmaster).'
|
'Configure newsletter dispatch on a per-tree basis. The sender is the contact user of each tree (falling back to the site webmaster).'
|
||||||
=> 'Newsletter-Versand pro Stammbaum konfigurieren. Absender ist die Kontaktperson des jeweiligen Baums (alternativ der Webmaster der Seite).',
|
=> 'Newsletter-Versand pro Stammbaum konfigurieren. Absender ist die Kontaktperson des jeweiligen Baums (alternativ der Webmaster der Seite).',
|
||||||
'Enable newsletter for this tree' => 'Newsletter für diesen Baum aktivieren',
|
'Enable newsletter for this tree' => 'Newsletter für diesen Baum aktivieren',
|
||||||
'Only intact marriages of still-living couples are included.'
|
'Only intact marriages of still-living couples are included.'
|
||||||
=> 'Nur bestehende Ehen lebender Paare werden berücksichtigt.',
|
=> 'Nur bestehende Ehen lebender Paare werden berücksichtigt.',
|
||||||
'Births and deaths of deceased people are included once per calendar month.'
|
|
||||||
=> 'Geburten und Todestage verstorbener Personen werden einmal pro Kalendermonat einbezogen.',
|
|
||||||
'Last sent: %s' => 'Zuletzt gesendet: %s',
|
'Last sent: %s' => 'Zuletzt gesendet: %s',
|
||||||
'Configure your system cron, systemd timer, or any external scheduler to call the URL below. The schedule decides when newsletters are actually due — calling more frequently is safe.'
|
'Configure your system cron, systemd timer, or any external scheduler to call the URL below. The schedule decides when newsletters are actually due — calling more frequently is safe.'
|
||||||
=> 'Richten Sie System-Cron, systemd-Timer oder einen externen Scheduler so ein, dass er die untenstehende URL aufruft. Der Versandplan entscheidet, wann tatsächlich gesendet wird — häufiger aufrufen ist unbedenklich.',
|
=> 'Richten Sie System-Cron, systemd-Timer oder einen externen Scheduler so ein, dass er die untenstehende URL aufruft. Der Versandplan entscheidet, wann tatsächlich gesendet wird — häufiger aufrufen ist unbedenklich.',
|
||||||
@@ -231,12 +236,6 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
$frequency = Validator::parsedBody($request)
|
$frequency = Validator::parsedBody($request)
|
||||||
->isBetween(Configuration::MIN_FREQUENCY_DAYS, Configuration::MAX_FREQUENCY_DAYS)
|
->isBetween(Configuration::MIN_FREQUENCY_DAYS, Configuration::MAX_FREQUENCY_DAYS)
|
||||||
->integer('frequency-' . $id, Configuration::DEFAULT_FREQUENCY_DAYS);
|
->integer('frequency-' . $id, Configuration::DEFAULT_FREQUENCY_DAYS);
|
||||||
$lookahead = Validator::parsedBody($request)
|
|
||||||
->isBetween(Configuration::MIN_LOOKAHEAD_DAYS, Configuration::MAX_LOOKAHEAD_DAYS)
|
|
||||||
->integer('lookahead-' . $id, Configuration::DEFAULT_LOOKAHEAD_DAYS);
|
|
||||||
$histLook = Validator::parsedBody($request)
|
|
||||||
->isBetween(7, 60)
|
|
||||||
->integer('historical-' . $id, Configuration::DEFAULT_HISTORICAL_LOOKAHEAD);
|
|
||||||
$lineal = Validator::parsedBody($request)
|
$lineal = Validator::parsedBody($request)
|
||||||
->isBetween(Configuration::MIN_LINEAL_DEPTH, Configuration::MAX_LINEAL_DEPTH)
|
->isBetween(Configuration::MIN_LINEAL_DEPTH, Configuration::MAX_LINEAL_DEPTH)
|
||||||
->integer('lineal-' . $id, Configuration::DEFAULT_LINEAL_DEPTH);
|
->integer('lineal-' . $id, Configuration::DEFAULT_LINEAL_DEPTH);
|
||||||
@@ -246,8 +245,6 @@ class Module extends AbstractModule implements ModuleCustomInterface, ModuleConf
|
|||||||
|
|
||||||
$tree->setPreference(Configuration::PREF_ENABLED, $enabled ? '1' : '0');
|
$tree->setPreference(Configuration::PREF_ENABLED, $enabled ? '1' : '0');
|
||||||
$tree->setPreference(Configuration::PREF_FREQUENCY_DAYS, (string) $frequency);
|
$tree->setPreference(Configuration::PREF_FREQUENCY_DAYS, (string) $frequency);
|
||||||
$tree->setPreference(Configuration::PREF_LOOKAHEAD_DAYS, (string) $lookahead);
|
|
||||||
$tree->setPreference(Configuration::PREF_HISTORICAL_LOOKAHEAD, (string) $histLook);
|
|
||||||
$tree->setPreference(Configuration::PREF_LINEAL_DEPTH, (string) $lineal);
|
$tree->setPreference(Configuration::PREF_LINEAL_DEPTH, (string) $lineal);
|
||||||
$tree->setPreference(Configuration::PREF_INCLUDE_ANNIVERSARIES, $annivs ? '1' : '0');
|
$tree->setPreference(Configuration::PREF_INCLUDE_ANNIVERSARIES, $annivs ? '1' : '0');
|
||||||
$tree->setPreference(Configuration::PREF_EXTRA_RECIPIENTS, $extras);
|
$tree->setPreference(Configuration::PREF_EXTRA_RECIPIENTS, $extras);
|
||||||
|
|||||||
@@ -86,18 +86,9 @@ final class NewsletterDispatchService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$due_at = Configuration::lastSentAt($tree)
|
// Per-tree "is anyone due?" is decided inside dispatchForTree
|
||||||
+ Configuration::frequencyDays($tree) * 86400;
|
// — each recipient has their own cadence and last-sent
|
||||||
|
// timestamp, so the gate is per-user, not per-tree.
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$log[] = $this->dispatchForTree($tree, $module, $now, $force);
|
$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
|
private function dispatchForTree(Tree $tree, ModuleInterface $module, int $now, bool $force): string
|
||||||
{
|
{
|
||||||
$include_anniversaries = Configuration::includeAnniversaries($tree);
|
$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
|
// One number controls everything: how often the newsletter is
|
||||||
// scheduled send of each calendar month. Forced sends (admin
|
// sent AND how far ahead each issue looks for events. Same
|
||||||
// hitting "Send now" to preview the newsletter) always include
|
// window applies to living birthdays/anniversaries and to the
|
||||||
// it — otherwise re-clicking the button silently strips the
|
// historical (deceased) section.
|
||||||
// section after the first run of the month.
|
$window = Configuration::frequencyDays($tree);
|
||||||
$include_historical = $force
|
|
||||||
|| Configuration::lastHistoricalMonth($tree) !== $current_month;
|
|
||||||
|
|
||||||
$birthdays = $this->event_query_service->upcomingBirthdays($tree, $lookahead);
|
$birthdays = $this->event_query_service->upcomingBirthdays($tree, $window);
|
||||||
$anniversaries = $include_anniversaries
|
$anniversaries = $include_anniversaries
|
||||||
? $this->event_query_service->upcomingAnniversaries($tree, $lookahead)
|
? $this->event_query_service->upcomingAnniversaries($tree, $window)
|
||||||
: null;
|
|
||||||
$historical = $include_historical
|
|
||||||
? $this->event_query_service->upcomingHistoricalEvents($tree, $historical_lookahead)
|
|
||||||
: null;
|
: null;
|
||||||
|
$historical = $this->event_query_service->upcomingHistoricalEvents($tree, $window);
|
||||||
|
|
||||||
// Suppress entirely empty newsletters so subscribers don't get
|
// Suppress entirely empty newsletters so subscribers don't get
|
||||||
// a near-empty email on a slow fortnight.
|
// a near-empty email on a slow fortnight.
|
||||||
@@ -172,6 +156,10 @@ final class NewsletterDispatchService
|
|||||||
// labels are personalised relative to whichever individual
|
// labels are personalised relative to whichever individual
|
||||||
// record the recipient is linked to in this tree.
|
// record the recipient is linked to in this tree.
|
||||||
foreach ($group as $recipient) {
|
foreach ($group as $recipient) {
|
||||||
|
if (!$this->recipientIsDue($tree, $recipient, $now, $force)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$relationships = $this->relationshipMap($tree, $recipient, $featured);
|
$relationships = $this->relationshipMap($tree, $recipient, $featured);
|
||||||
$detailed_set = $this->detailedXrefs($tree, $recipient, $featured);
|
$detailed_set = $this->detailedXrefs($tree, $recipient, $featured);
|
||||||
|
|
||||||
@@ -189,9 +177,7 @@ final class NewsletterDispatchService
|
|||||||
'anniversaries' => $anniversaries,
|
'anniversaries' => $anniversaries,
|
||||||
'historical' => $historical,
|
'historical' => $historical,
|
||||||
'include_anniversaries' => $include_anniversaries,
|
'include_anniversaries' => $include_anniversaries,
|
||||||
'include_historical' => $include_historical,
|
'window_days' => $window,
|
||||||
'lookahead_days' => $lookahead,
|
|
||||||
'historical_lookahead' => $historical_lookahead,
|
|
||||||
'generated_at' => $now,
|
'generated_at' => $now,
|
||||||
'avatar_cids' => $avatar_cids,
|
'avatar_cids' => $avatar_cids,
|
||||||
'relationships' => $relationships,
|
'relationships' => $relationships,
|
||||||
@@ -204,6 +190,12 @@ final class NewsletterDispatchService
|
|||||||
try {
|
try {
|
||||||
if ($this->mailer->sendWithEmbeds($from, $recipient, $from, $subject, $text, $html, $recipient_avatars)) {
|
if ($this->mailer->sendWithEmbeds($from, $recipient, $from, $subject, $text, $html, $recipient_avatars)) {
|
||||||
$sent++;
|
$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 {
|
} else {
|
||||||
$failures++;
|
$failures++;
|
||||||
}
|
}
|
||||||
@@ -219,20 +211,43 @@ final class NewsletterDispatchService
|
|||||||
I18N::init($original_locale);
|
I18N::init($original_locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
Configuration::setLastSentAt($tree, $now);
|
||||||
if ($include_historical) {
|
|
||||||
Configuration::setLastHistoricalMonth($tree, $current_month);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprintf(
|
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(),
|
$tree->name(),
|
||||||
$sent,
|
$sent,
|
||||||
$failures,
|
$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
|
* Resolve a "highlighted" image for every individual mentioned in
|
||||||
* the newsletter and return a CID-keyed map of bytes + MIME type
|
* the newsletter and return a CID-keyed map of bytes + MIME type
|
||||||
|
|||||||
Reference in New Issue
Block a user