Skip to content

通知のカスタムフォーマットとリマインダーの内部動作 ​

第2版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

「テーブルの管理 → 通知」の本文(カスタムフォーマット)は、1 行ずつ、項目の書式を表す JSON として読まれ、読めなかった行だけが {Url} などの置換を受けてそのまま出力されます。この仕組みを知らないと、「{Url} が展開されない」「[Title] が置き換わらない」といった現象の理由が分かりません。 このページでは、通知の本文・件名・宛先の組み立て方と、リマインダーの実行タイミング・日付項目・タイムゾーンの扱いを、1.5.8.1 のソースで確かめた内容でまとめます。

要点

  • 本文の各行は {"Name":"[Title]"} のような JSON として読まれる。項目が特定できた行はその項目の値に、それ以外の行は {Url}・{LoginId}・{UserName}・{MailAddress} の 4 つだけを置換して出力する
  • [項目名] がそのまま置き換わるのは件名と、プロセスの通知の件名・本文だけ
  • JSON の Prefix などに書いた {Url} は展開されない
  • リマインダーは Service.json の TimeZoneDefault の時計で動く。サーバーの OS のタイムゾーンと違うと、送信時刻がその時差だけずれる

通知の本文の組み立て ​

既定の書式とカスタムフォーマット ​

「カスタムフォーマットを使う」にチェックを入れて保存したときだけ、入力した書式が Format として保存されます(既定の書式と同じ内容なら保存されません)。Format が空のときは、次の既定の書式が使われます(Notification.cs)。

text
{Url}
{"Name":"[Title]"}
{"Name":"[Body]"}
{"Name":"[Status]"}
…(通知対象の項目ごとに 1 行)

{UserName}<{MailAddress}>

1 行ずつ JSON として読む ​

本文は NoticeBody が作ります。書式を \n で分割し、各行の前後の空白を除いてから NotificationColumnFormat として JSON を読みます(ResultModel.cs)。

図を読み込み中…

  • JSON として読めない行({Url} だけの行、普通の文章など)は、例外を握りつぶして null 扱いになり、そのまま出力されます(Jsons.cs)。
  • JSON として読めても、Name から項目が特定できなければ同じく「そのまま出力」の側に入ります。{} だけの行や、Name を持たない JSON(Adaptive Card の JSON など)はこちらです。
  • 項目が特定できた行は、その項目の値に置き換わり、行の元の文字列は出力されません。
  • 本文は Issues(期限付きテーブル)・Results(記録テーブル)・Wikis で同じ構造で、CodeDefiner のテンプレート(Model_Notice_Body.txt)から生成されています。

項目の特定は SiteSettings.IncludedColumns が行います。Name の中から正規表現 (?<=\[).+?(?=\]) で角括弧の中身を取り出し、項目名(ClassA など)と完全一致する項目を探します(SiteSettings.cs)。表示名(ラベル)では一致しません。

HttpClient 通知に複数行の JSON を書いてもよい

各行は前後の空白を除かれ、\n でつなぎ直されます。JSON では改行は空白と同じ扱いなので、複数行に整形した JSON もそのまま有効な JSON として送られます。困るのは、ある 1 行だけで Name に [項目名] を含む JSON オブジェクトとして読めてしまう場合です(その行は項目の値に置き換わります)。Newtonsoft.Json はプロパティ名の大文字・小文字を区別しないため、"name" も Name として読まれます。

項目の値の出し方(ToNotice) ​

特定できた項目は、型ごとの ToNotice で文字列にします(ToNoticeExtensions.cs)。

  • 選択肢を持つ項目(リンク項目を含む)は、値を選択肢の表示名に変換します。複数選択はカンマと空白(, + 半角空白)でつなぎます(ToNoticeExtensions.cs)。リンク項目の表示名を出すのに特別な書き方は要りません。
  • 日時は操作したユーザーのタイムゾーンに変換してから表示形式に合わせます(ToNoticeExtensions.cs)。
  • NoticeBody の分岐にあるのは、タイトル・内容・状況・管理者・担当者・ロック・コメント・作成者・更新者と、分類・数値・日付・説明・チェック・添付ファイルの各項目です。期限付きテーブルでは開始・完了・作業量・進捗率も加わります。ID や更新日時などは項目として特定されても分岐が無いため、何も出力されません(ResultModel.cs)。

更新時の通知では、その項目が変わっていない行は出力されません(Always が true なら出します)。変わった行は既定で「変更前 => 変更後」の形になります(NotificationColumnFormat.cs)。

書式行の JSON プロパティ ​

NotificationColumnFormat のプロパティと、未指定のときの動きです(NotificationColumnFormat.cs)。

プロパティ未指定のとき内容
Name―[項目名]。これが無いと項目行にならない
Prefix空見出し(項目の表示名)の前に付ける文字列
Delimiter" : "見出しと値の区切り
Allow" => "変更前と変更後の区切り
DiffTypesstandardDiffMatchPatch にすると、変更前後を並べずに差分を記号で示す
StartBracket / EndBracket( / )DiffMatchPatch の差分を囲む括弧
DeletePrefixSymbol / DeleteSuffixSymbol- / 空削除された部分の前後に付ける記号
AddPrefixSymbol / AddSuffixSymbol+ / 空追加された部分の前後に付ける記号
Always出さないtrue なら、更新時に変わっていなくても出す
DisplayTypes変更前後0 変更前後、1 変更前だけ、2 変更後だけ
ValueOnly見出しを出すtrue なら見出し(Prefix・表示名・区切り)を出さない
ConsiderMultiLine改行するMarkdown の項目で、見出しと値・変更前後の間に改行を入れる。false で入れない
json
{"Name":"[Title]","Always":true,"ValueOnly":true}
{"Name":"[Status]","DisplayTypes":2}
{"Name":"[Body]","DiffTypes":"DiffMatchPatch","StartBracket":"【","EndBracket":"】","DeletePrefixSymbol":"削除:","AddPrefixSymbol":"追加:"}

件名・宛先・プロセスの通知 ​

場所[項目名]{Url} など 4 つ処理
本文(通知)書式行の Name だけ項目行以外の行だけNoticeBody
件名(通知)置換する置換するReplacedDisplayValues
件名・本文(プロセスの通知)置換する置換するReplacedDisplayValues
宛先・CC・BCCユーザーのメールアドレスに解決置換しないReplacedAddress
プレフィックス置換しない置換しない件名の前にそのまま連結
  • 件名は [項目名] を項目の表示値に置き換え、そのあと {Url} などを置換します(ResultModel.cs)。件名の [NotificationTrigger] は、作成・更新・削除に応じた語に置き換わります。件名が空なら「"タイトル" を作成しました。」のような既定の件名になります(ResultModel.cs)。
  • プロセスの通知は、件名も本文も ReplacedDisplayValues を通るので、本文中の [項目名] がそのまま値に置き換わります(ResultModel.cs)。
  • 宛先の [Manager]・[Owner]・[ClassA] などは、その項目の値のユーザーのメールアドレスに置き換わります。メール通知では [RelatedUsers] で管理者・担当者・作成者・更新者をまとめて宛先にできます(Notification.cs、Notification.cs)。
  • プレフィックスは、メールでは件名の先頭に、Slack などでは *プレフィックス件名* の形でそのまま連結されます(Notification.cs)。HttpClient 通知は本文だけを送り、件名もプレフィックスも使いません(Notification.cs)。

{Url} などの 4 つの置換内容です(ResultModel.cs)。

変数置換内容
{Url}レコードの編集画面の絶対 URL
{LoginId}操作したユーザーのログイン ID
{UserName}操作したユーザーの表示名
{MailAddress}操作したユーザーのメールアドレス

置き換わらない書き方 ​

書き方結果理由
{"Name":"[Title]","Prefix":"{Url} "}{Url} がそのまま出る項目行は ReplacedContextValues を通らない。Delimiter・Allow・括弧・記号も同じ
{"Name":"[Title]"}{Url}JSON がそのまま出て、{Url} だけ置換される1 行に JSON と文字列が続くと JSON として読めず、項目行にならない
{}{} がそのまま出るJSON としては読めるが Name が無く、4 つの変数のどれにも一致しない
本文の行に [Title](JSON でない行)[Title] がそのまま出る項目の置換は書式行の Name だけ。件名では置き換わる
件名に [@ClassA]そのまま出る項目は見つかるが、置換は [ClassA] の文字列で行うため一致しない(ResultModel.cs)
[ClassA~1234,ClassB](リンク先の項目)項目行にならない・件名でも置換されないIncludedColumns は自サイトの項目名と完全一致で探し、リンク先の項目を解決しない
{Url} などの値が無い空文字に置き換わる置換値が null だと string.Replace は削除と同じになる(メールアドレス未設定のユーザーなど)

1 行に JSON と文字列を続けたときに JSON として読めないのは、JsonConvert.DeserializeObject が JSON の後ろに余分な内容があると例外にするためです(1.5.8.1 の Newtonsoft.Json は 13.0.4)。値の前に URL を出したいときは、{Url} を独立した行に書きます。

リンク先の項目を通知に出す改修案と、Prefix などの {Url} を展開する改修案は 通知のプレースホルダの改修案 にまとめています。

リマインダー ​

実行の流れと間隔 ​

リマインダーは次の 2 通りのどちらかで動きます。

方式条件間隔
バックグラウンドサービス(Quartz)BackgroundService.json の Reminder が trueReminderCheckIntervalSeconds(既定 60 秒。30 〜 3600 秒に丸められ、範囲外なら Warning を SysLogs に記録)
URL の呼び出しReminder.json の Enabled が true で、上が false(既定)外部から /reminderschedules/remind を GET で呼んだとき(匿名でアクセス可)

(ReminderBackgroundTimer.cs、BackgroundService.json、ReminderSchedulesController.cs)

1 回の実行では、ReminderSchedules テーブルから ScheduledTime が現在時刻以前のものを取り出して順に送り、送るたびに ScheduledTime を次回の時刻に更新します。対象が無くなるか Reminder.json の Span(既定 30 秒)を過ぎるまで繰り返します(ReminderScheduleUtilities.cs、Reminder.cs)。

Reminder.json既定値意味
Interval5001 件送るごとに待つミリ秒。送信間隔(毎日・毎週)とは関係ない
Span301 回の実行で繰り返す秒数
Limit10001 通に載せるレコードの上限

送信の間隔は、リマインダーの「開始日時」と「繰り返し」(毎日・毎週など)から計算した ScheduledTime だけで決まります。

  • 送信はサイトの最終更新者(Sites.Updator)のユーザーとして行われ、サーバースクリプトは動きません(ReminderScheduleUtilities.cs)。
  • 送信中に例外が起きると、そのリマインダーのスケジュールを削除し、エラー内容をリマインダーの宛先にメールします。次回からは送られません(Reminder.cs)。

日付の項目 ​

リマインダーは、指定した日付の項目(「項目」欄)を基準に「今日から Range 日以内」のレコードを集め、日付ごとにまとめて本文を作ります(Reminder.cs)。

  • 選べるのは日時型で、作成日時・更新日時ではない項目だけです。空の選択肢はありません(SiteSettings.cs、SiteUtilities.cs)。
  • 未指定なら CompletionTime(完了)を使います。記録テーブルのように CompletionTime が無いと項目が決まらず、GetDataTable で例外になって、上のとおりスケジュールが削除されます(Reminder.cs)。
  • 条件は「日付 < 今日 + Range 日」で、ビューの条件に AND で付きます。日付が空のレコードは、ビューの条件に合っても対象になりません(SQL で NULL との比較は偽になるため)。「期限切れを除く」「過去に完了したものも送る」の条件も同じ日付の項目で比べます。

日付の項目を使わず、ビューの条件だけで送る改修案は リマインダーの改修案 にまとめています。

タイムゾーン ​

リマインダーの処理で使う context はログインを経ないため、タイムゾーンは Service.json の TimeZoneDefault(配布時の既定は UTC)のままです(Context.cs、Context.cs)。このため次のようになります。

処理基準の時計
開始日時入力した値をそのまま保存(タイムゾーン変換なし)
保存時の最初の ScheduledTime開始日時から、保存したユーザーのタイムゾーンの「今」以降で最初の回(SiteModel.cs)
送るかどうかの判定ScheduledTime <= DateTime.Now.ToLocal(context)、つまり TimeZoneDefault の「今」(ReminderScheduleUtilities.cs)
送信後の次回 ScheduledTime開始日時から、TimeZoneDefault の「今」以降で最初の回(Times.cs)
対象レコードの範囲(今日 + Range 日、過去の完了)TimeZoneDefault の「今日」の 0 時を、サーバーローカルで格納された日付と直接比べる

開始日時は「TimeZoneDefault の時計で何時か」として扱われます。TimeZoneDefault が UTC のままでサーバーや利用者が日本時間なら、14:56 に設定した毎日のリマインダーは日本時間の 23:56 に届きます。判定と次回の計算は同じ context で行うため、確認したソースでは、この時差で同じリマインダーが続けて送られる経路は見当たりません。

TimeZoneDefault をサーバーの OS のタイムゾーン(日本なら Tokyo Standard Time)に合わせると、上の変換はすべて恒等変換になり、ずれは起きません(Times.cs)。タイムゾーン全体の仕組みは タイムゾーンの考え方 を参照してください。

関連ページ ​

変更履歴

第2版記事の確認版を繰り返す表現を整理する
第1版通知とリマインダーの書式・置き換わらない書き方・タイムゾーンの影響、システムログの外部通知の解説と、関連する改修・設計メモを追加