リマインダーの改修案(日付の項目なし・タイムゾーン)
リマインダーについて、次の 2 つを本体の改修で実現する案です。ここにあるのは本体の標準機能ではありません。
- 日付の項目を指定せず、ビューの条件に合うレコードをそのまま送る
Service.jsonのTimeZoneDefaultに依存せずに送信時刻を決める
現行の動きは 通知のカスタムフォーマットとリマインダーの内部動作 を参照してください。
1. 日付の項目を必須にしない
前提にした現行実装(1.5.8.1)
日付の項目(Reminder.Column)は次の 3 か所で使われ、事実上必須です(Reminder.cs)。
| メソッド | 使い方 |
|---|---|
GetColumn | 未指定なら CompletionTime にする。無ければ null(L157-L163) |
GetDataTable | 取得列・「日付 < 今日 + Range 日」・「期限切れを除く」・「過去に完了したものも送る」・並び順のすべてにこの項目を使う(L520-L623) |
GetBody | レコードをこの項目の日付でまとめ、「日付 (3 日後)」のような見出しを付ける(L455-L518) |
設定画面の「項目」は日時型で作成日時・更新日時でない項目だけの選択肢で、空の選択肢はありません(SiteSettings.cs、SiteUtilities.cs)。ビューの条件(Condition)は日付の項目とは独立に WHERE 句になっています。
変更点
| # | ファイル | 変更 |
|---|---|---|
| 1 | Models/Sites/SiteUtilities.cs | ReminderColumn のドロップダウンに insertBlank: true を付け、空を選べるようにする |
| 2 | Libraries/Settings/Reminder.cs の GetDataTable | Column が空なら、取得列に日付の項目を足さず、日付の 3 つの条件を付けない。並び順は ID の降順などにする |
| 3 | Libraries/Settings/Reminder.cs の GetBody | Column が空なら日付でまとめず、1 行ずつ Line とリンクを並べる |
| 4 | 設定画面のスクリプト | Column が空のときは「Range」「期限切れを除く」「過去に完了したものも送る」を無効にする(どれも日付の項目が前提) |
GetColumn の CompletionTime へのフォールバックは、空を「未設定」ではなく「日付の項目なし」と区別できるように扱いを決めておく必要があります。現行は null のときだけフォールバックするので、空文字を「なし」とする方法が既存の設定と両立します。
GetDataTable・GetBody の変更イメージ
// GetDataTable
var orderByColumn = !Column.IsNullOrEmpty()
? ss.GetColumn(context: context, columnName: Column)
: null;
var column = new SqlColumnCollection()
.Add(column: ss.GetColumn(
context: context,
columnName: Rds.IdColumn(ss.ReferenceType)));
if (orderByColumn != null) column.Add(column: orderByColumn);
column.ItemTitle(ss.ReferenceType);
// …(宛先・件名・本文・Line の項目の追加は現行どおり)
var where = view.Where(
context: context,
ss: ss,
checkPermission: false,
requestSearchCondition: false);
if (orderByColumn != null)
{
// 現行の「日付 < 今日 + Range」「期限切れを除く」「状況 / 過去に完了」の条件を追加
}
var orderBy = new SqlOrderByCollection().Add(
column: orderByColumn ?? ss.GetColumn(
context: context,
columnName: Rds.IdColumn(ss.ReferenceType)),
orderType: SqlOrderBy.Types.desc);// GetBody(日付の項目なし)
dataRows.ForEach(dataRow =>
{
sb.Append(ReplacedLine(context: context, ss: ss, dataRow: dataRow, line: Line));
if (NotSendHyperLink != true)
{
sb.Append("\n\t", Locations.ItemEditAbsoluteUri(
context: context,
id: dataRow.Long(Rds.IdColumn(ss.ReferenceType))));
}
sb.Append("\n");
});注意点
- 現行でも、日付が空のレコードは SQL の比較が偽になるため対象になりません。日付の項目なしの経路では、これらもビューの条件だけで対象になります。
GetBodyの日付でのまとめを通さずに空の日付を扱うと、DataRow.DateTimeが空の値を1899/12/30に変換する(DataRows.cs、Types.cs)ため、見出しに不正な日付が出ます。日付の項目なしのときは必ず 3 の分岐を通します。- 送る件数の上限は
Reminder.jsonのLimit(既定 1000)のままです。日付で絞らない分、ビューの条件で対象を絞っておきます。 Columnを設定済みの既存のリマインダーは従来どおり動きます。スキーマの変更は要りません(リマインダーはサイト設定の JSON に保存されます)。
2. タイムゾーンに依存しない送信時刻
前提にした現行実装(1.5.8.1)
リマインダーを送る処理の context はログインを経ないため、タイムゾーンは TimeZoneDefault です(Context.cs)。
- 送るかどうかは
ScheduledTime <= DateTime.Now.ToLocal(context)で判定する(ReminderScheduleUtilities.cs) - 次回の
ScheduledTimeは、開始日時(入力値のまま保存)からTimeZoneDefaultの「今」以降で最初の回(Times.cs) - 保存時の最初の
ScheduledTimeだけは、保存したユーザーのタイムゾーンの「今」を使う(SiteModel.cs) - 対象レコードの「今日 + Range 日」は
TimeZoneDefaultの今日を、サーバーローカルで格納された日付とそのまま比べる。「期限切れを除く」の基準(ScheduledTime)はToUniversalでサーバーローカルに戻してから比べる(Reminder.cs)
つまり開始日時は「TimeZoneDefault の時計で何時か」として扱われ、TimeZoneDefault とサーバーの OS のタイムゾーン・利用者のタイムゾーンが違うと、送信時刻がその時差だけずれます。判定と次回の計算は同じ context で行うため、1.5.8.1 のソースでは、この時差で同じリマインダーが続けて送られる経路は確認できませんでした。
運用で避けるなら、TimeZoneDefault をサーバーの OS のタイムゾーンに合わせます。変換が恒等変換になり、改修は要りません。
案の比較
| 案 | 内容 | 変更量 | 複数タイムゾーン |
|---|---|---|---|
| 1 | リマインダー用の context の TimeZoneInfo を TimeZoneInfo.Local にする | 小(ReminderScheduleUtilities の 2 か所の context 生成の後に 1 行ずつ) | サーバーの時計に揃うだけ |
| 2 | Reminder にタイムゾーン ID を持たせ、そのリマインダーの処理だけその context で行う | 中(プロパティ・設定画面・保存処理) | リマインダーごとに指定できる |
案 1 は、判定・次回の計算・対象レコードの範囲がすべてサーバーローカルの時計に揃い、ToLocal / ToUniversal が恒等変換になります。開始日時の意味は「サーバーの時計で何時か」に変わるため、TimeZoneDefault を UTC のまま運用していた環境では、既存のリマインダーの送信時刻が変わります。
ScheduledTime を選ぶ最初の判定(Remind(Context context) の中)は、外側の context を使います。ここも同じタイムゾーンにしないと、判定と次回の計算の時計がずれて、送ったのに ScheduledTime が「今」より前に残る状態になり得ます。案 1 では、外側(ReminderBackgroundTimer の CreateContext と、URL 呼び出しの ReminderSchedulesController)と内側(ReminderScheduleUtilities.Remind(Context, DataRow))の両方で揃えます。
// ReminderScheduleUtilities.Remind(Context context, DataRow dataRow) の context 生成の後
context.TimeZoneInfo = TimeZoneInfo.Local;案 2 は、Reminder クラスに TimeZoneId を足し、GetRecordingData で保存、設定画面に選択欄を足します。リマインダーはサイト設定の JSON に保存されるので、テーブルの変更は要りません。未設定の既存のリマインダーは現行どおり TimeZoneDefault にすれば、送信時刻は変わりません。ScheduledTime の判定を SQL で一括に行っている部分は、リマインダーごとにタイムゾーンが違うと 1 本の比較で済まなくなるため、ScheduledTime を保存するときにサーバーローカルへ変換して持つ、などの設計が要ります。
確かめること
- 変更前に登録済みのリマインダーの
ScheduledTimeを再計算するか(サイトを更新すると全リマインダーのScheduledTimeが作り直される。SiteModel.cs) - 他のバックグラウンド処理(
DeleteSysLogsTimerなど)には影響しないこと(リマインダーのcontextだけを変える) - 複数台構成で OS のタイムゾーンが揃っていること(案 1 はサーバーの時計に依存する)