• tracとGraphvizPlugin

    {% raw %} pukiwikiにGraphvizプラグインというのがある。
    graphvizはdot言語(?)で記述した構造を画像化するソフトだ。これをwikiのプラグインから使うというもの。 wikiで編集したdot言語が図に置き換わる。

    ここのブログの以下のエントリで使っている図はgraphvizで描いたものだ。

    導入

    • graphvizとlibrsvg2をインストールする。FC3、FC5の場合yumなりでインストールできちゃうはず。
      librsvg2はSVG出力しないか、pngでアンチエイリアスを使わない場合不要っぽいが、うちの環境では何かの依存関係ですでにインストールされていた。
    • GraphvizPluginをダウンロードする。
      今回使用したのは0.9
    • ダウンロードしたアーカイブをどこか適当な場所に展開する。
    • 展開先/graphvizplugin/0.9/ というディレクトリがあるはずなのでcdし「python setup.py bdist_egg」
      展開先/graphvizplugin/0.9/dist/graphviz-0.6.9-py2.3.egg というファイルが出来たはずなので、tracプロジェクトのplugins/ディレクトリにコピーする。 細かいことはREADMEに書いてある。
    • Graphviz Pluginはdotから生成した画像を保存する場所(=IMGのSRCのリンク先)が必要なのでこれを用意する。
      tracdもしくはwebサーバから書き込み可能にしなければならない。環境に応じて適宜。
      うちの環境では以下のディレクトリを使うことにした。
      tracプロジェクト/htdocs/graphviz/
    • trac.iniに[graphviz]セクションを追加する。
      うちの環境では以下の通り。他にもいろいろ設定できるが環境によって必須なのはgraphvizなどのコマンドのpath。
      [graphviz]
      cache_dir = tracプロジェクト/htdocs/graphviz
      png_antialias = 1
    • ここの環境ではtracdを使っているので再起動した。
    Graphvizプラグインを導入し、tracのwikiに以下のように記述すると、
    {{{
    #!graphviz
    digraph G {
        graph [rankdir="LR" size="6,4"]
        t1 [ label="明日は" ]
        t2 [ label="明日の" ]
        t3 [ label="風が" ]
        t4 [ label="吹く" ]
        t5 [ label="吹けば" ]
        t6 [ label="桶屋が" ]
        t7 [ label="儲かる" ]
        t1 -> t2 -> t3 -> t4
        t3 -> t5 -> t6 -> t7
        a->b->c
    }
    }}}
    

    という画像が生成される。日本語も問題ない。

    dot言語については本家Graphviz チュートリアルがわかりやすい。 {% endraw %}
  • tracからやって来るメール その2

    tracから送られてくるメールのDateフィールドの値は、タイムゾーンがGMTになっている。
    Date: Sun, 16 Jul 2006 14:29:21 -0000
    タイムゾーンを無視しているメーラーがある。メーラーというかwebメールなのだが。あきらかにメーラーが悪いが、かといって9時間ずれたメールが並んでいるのはなんだか嫌だ。ということで、送信メールのDateフィールドにlocaltimeを適用するパッチを追加。
    Date: Sun, 16 Jul 2006 23:32:00 +0900
    ダウンロード→trac.patch.txt

    元のソースは、インタアクト株式会社が日本語化したtrac-0.9.5-ja-1。

    > 元のソースを展開する
    > パッチをDLしてどこかに置く
    > cd trac-0.9.5-ja-1/ (展開したソースディレクトリにcd)
    > patch -p1 < DLしたパッチ
    # python setup.py install
    (で、いけるはず)

    設定例

    [notification]
    always_notify_owner = true
    smtp_always_cc =
    smtp_password =
    smtp_enabled = true
    smtp_replyto = xxxxxxx@example.com
    smtp_port = 25
    always_notify_reporter = true
    smtp_server = SMTPサーバ
    smtp_from = xxxxxxx@example.com
    smtp_user =
    use_localtime = true
  • tracパッチまとめ

    以下のエントリで書いたパッチをひとくくりにしたもの。
    ダウンロード→trac.patch.txt

    元のソースは、インタアクト株式会社が日本語化したtrac-0.9.5-ja-1。

    > 元のソースを展開する
    > パッチをDLしてどこかに置く
    > cd trac-0.9.5-ja-1/ (展開したソースディレクトリにcd)
    > patch -p1 < DLしたパッチ
    # python setup.py install
    (で、いけるはず)
  • tracからやって来るメール

    trac.iniでsmtp_enabledをtrueにsmtp_fromかsmtp_replytoにアドレスを設定すれば、でチケットを登録したり更新した際に関係者にメールが送られる。
    このメールについて、自分の趣味とは合わないな~と思っていること。

    その1 - From:の表示名とアドレス

    tracからのメールは以下のように「プロジェクトさん」から送られてきたことになる。
    From: [trac]セクションのname値 <[notification]セクションのsmtp_from値>
    一人でTODOやアイデア管理に使っている分には特に気にならないが、複数の利用者が1つのチケットに対してあれこれ変更や追記をすると、受信したメールの発信者が全て「プロジェクトさん」のため、メーラで整理しにくい。
    そもそもSubjectにプロジェクト名が入っているので、メールの一覧表示における情報量に無駄があるなあ、と。
    ※ちなみに、tracから来たメールはプロジェクトごとに特定のフォルダに振り分けて、スレッド表示にしている。
    • From:フィールドの表示名を報告者の名前にする
      • といっても、on/offの制御はしたいところなので、trac.iniの[notification]セクションにuse_reporter_name=trueを設定すると、表示名が報告者の名前になるようにする。falseか未設定なら元の通りプロジェクト名が表示名になる。
      • 「報告者の名前」は、以下の優先順位で適用される
        1. 「ユーザ設定」で「名前」に記入した文字列
        2. 認証名(ログイン状態で「xxxxとしてログイン中」と出るアレ)
    • From:フィールドのメールアドレスを報告者のアドレスにする
      • 表示名と同様に、trac.iniの[notification]セクションにuse_reporter_email=trueを設定すると、Fromアドレスが報告者のアドレスになるようにする。falseか未設定なら元の通りプロジェクト名が表示名になる。
      • 結果的にFromの詐称を行うことになるので、smtpサーバによってはメール送信を蹴ってしまうかもしれない。(自ドメイン以外のメールアドレスがFrom:にあると蹴るケースなど)
      • smtp_fromに指定したアドレスはMAIL FROMとしてsmtpセッションで使われ、結果的にSender:の値になる。ウチで使っている環境に関して言うと、smtp_fromにsmtpサーバにとってローカルなメールアドレスを指定しておけば、From:が他ドメインでも送信してくれた。

    その2 - From:の表示名とあて先の表示名をMIMEエンコードしない

    特にanonymousで使っていて、「ユーザ設定」で日本語の名前を設定している場合、表示名部分をMIMEエンコードしないので文字化けしてしまう。先のエントリにも書いたようにウチの環境はanonymousでは使っておらず、認証名も英字のみとしているので、あまり問題ないのかも。
    • 送信先が「表示名 <アドレス>」の場合、表示名部分をMIMEエンコードする。判定はかなり適当。
    • 文字セットはSubjectを踏襲してUTF-8として、手抜きした。pythonでiso-2022-jpに変換するのって簡単なのかな。

    その3 - Message-ID:の生成の仕方

    プロジェクトのURLやチケットのIDを用い、ハッシュをとったもの+αにsmtp_fromの@以降を付与している。smtpサーバに外部のサーバを使用している場合にどうも気持ち悪い。
    smtp_fromがxxxxxxx@example.comだと、
     Message-ID: <071.56dd0b824d3625a8ce2b492402b465db@example.com>
    となる。
     Message-ID: <071.56dd0b824d3625a8ce2b492402b465db@tracサーバのFQDN>
    としたい。
    • trac.iniの[notification]セクションで、use_fqdn4message_id=trueに設定した場合、Message-ID:の@以降をtracをホストしているサーバのFQDNにする。
    • falseか未設定なら従来通りsmtp_fromの@以降。

    今回のパッチ

    --- trac-0.9.5-ja-1/trac/Notify.py 2006-02-16 10:47:21.000000000 +0900
    +++ trac-0.9.5-ja-1kai/trac/Notify.py 2006-06-25 00:07:56.000000000 +0900
    @@ -20,9 +20,12 @@
     from trac.web.clearsilver import HDFWrapper
     from trac.web.main import populate_hdf
     
    +import re
    +import socket
     import md5
     import time
     import smtplib
    +from email.Header import Header
     
     
     class Notify:
    @@ -74,6 +77,8 @@
         server = None
         email_map = None
         template_name = None
    +    reporter_name = ''
    +    reporter_email = ''
     
         def __init__(self, env):
             Notify.__init__(self, env)
    @@ -117,6 +122,17 @@
             if self.user_name:
                 self.server.login(self.user_name, self.password)
     
    +    def modifyNotification(self , authname , reporter_name_ , reporter_email):
    +        if self.env.config.getbool('notification', 'use_reporter_name'):
    +            if reporter_name_:
    +                self.reporter_name = reporter_name_
    +            elif authname:
    +                self.reporter_name = authname
    +
    +        if self.env.config.getbool('notification', 'use_reporter_email'):
    +            if reporter_email:
    +                self.reporter_email = reporter_email
    +
         def send(self, rcpt, mime_headers={}):
             from email.MIMEMultipart import MIMEMultipart
             from email.MIMEText import MIMEText
    @@ -132,7 +148,18 @@
             msg['X-Trac-Project'] =  projname
             msg['X-URL'] =  self.config.get('project','url')
             msg['Subject'] = Header(self.subject, 'utf-8')
    -        msg['From'] = '%s <%s>' % (projname, self.from_email)
    +
    +        display_name = projname
    +        from_email_ = self.from_email
    +        if self.env.config.getbool('notification', 'use_reporter_name') and \
    +            self.reporter_name:
    +            display_name = self.reporter_name
    +
    +        if self.env.config.getbool('notification', 'use_reporter_email') and \
    +            self.reporter_email:
    +            from_email_ = self.reporter_email
    +        
    +        msg['From'] = '%s <%s>' % (Header(display_name, 'utf-8'), from_email_)
             msg['Sender'] = self.from_email
             msg['Reply-To'] = self.replyto_email
             msg['To'] = rcpt
    @@ -301,6 +328,11 @@
             emails = []
             for recipient in recipients:
                 if recipient.find('@') >= 0:
    +                p = re.compile('([^<]*)\s*(<.+>)')
    +                m = p.match(recipient)
    +                if m:
    +                    recipient = '%s %s' % \
    +                        (Header(m.group(1), 'utf-8'), m.group(2))
                     emails.append(recipient)
                 elif self.email_map.has_key(recipient):
                     emails.append(self.email_map[recipient])
    @@ -312,12 +344,20 @@
                     result.append(e)
             return result
     
    +    def get_message_id_host(self):
    +        if self.config.getbool('notification', 'use_fqdn4message_id'):
    +            fqdn = socket.getfqdn()
    +            if fqdn:
    +                return fqdn
    +
    +        return self.from_email[self.from_email.find('@') + 1:]
    +
         def get_message_id(self, rcpt, modtime=0):
             """Generate a predictable, but sufficiently unique message ID."""
             s = '%s.%08d.%d.%s' % (self.config.get('project', 'url'),
                                    int(self.ticket.id), modtime, rcpt)
             dig = md5.new(s).hexdigest()
    -        host = self.from_email[self.from_email.find('@') + 1:]
    +        host = self.get_message_id_host()
             msgid = '<%03d.%s@%s>' % (len(s), dig, host)
             return msgid
     
    url: archives/2006/06/trac-060625.html --- trac-0.9.5-ja-1/trac/ticket/web_ui.py 2006-02-16 10:59:33.000000000 +0900
    +++ trac-0.9.5-ja-1kai/trac/ticket/web_ui.py 2006-06-24 22:28:52.000000000 +0900
    @@ -139,6 +139,8 @@
             # Notify
             try:
                 tn = TicketNotifyEmail(self.env)
    +            tn.modifyNotification(req.authname, \
    +                req.session.get('name', None), req.session.get('email', None))
                 tn.notify(ticket, newticket=True)
             except Exception, e:
                 self.log.exception("Failure sending notification on creation of "
    @@ -347,6 +349,8 @@
     
             try:
                 tn = TicketNotifyEmail(self.env)
    +            tn.modifyNotification(req.authname, \
    +                req.session.get('name', None), req.session.get('email', None))
                 tn.notify(ticket, newticket=False, modtime=now)
             except Exception, e:
                 self.log.exception("Failure sending notification on change to "

    設定例

    [notification]
    always_notify_owner = true
    smtp_always_cc =
    smtp_password =
    smtp_enabled = true
    smtp_replyto = xxxxxxx@example.com
    smtp_port = 25
    always_notify_reporter = true
    smtp_server = SMTPサーバ
    smtp_from = xxxxxxx@example.com
    smtp_user =
    use_reporter_name = true
    use_reporter_email = true
    use_fqdn4message_id = true
    あとは、SubjectかどこかでチケットのStatusがわかるといいんだけど、と思っているがとりあえずここまで。
  • tracプロジェクト全域で認証

    前回のパッチで、「tracdによるtracプロジェクトをhttpsでapacheの認証下(basic/digest以外)で公開する。そしてapacheで認証したユーザでtracにログインする」というところまで出来た。
    tracプロジェクトでanonymousを許す場合、apacheの認証を「/login」にだけに設定しておけばよい。だけど、自分の環境では「tracプロジェクト全域」に認証をかけているため、事実上anonymousを利用することはない。したがって、一度認証を通過しているにも関わらずいちいち「ログイン」を選択しないとログイン状態にできないのは面倒だ(特に、チケットの発行や「私のチケット」を見るとき)

    ということで、tracプロジェクト全域を認証して使う場合に、いちいち"ログイン"を選ばなくてもいいようにする。
    ちなみに、権限をdropしてanonymousで利用できなくする、というのとは目的が違うので注意。

    1. 以下のパッチをあてる。
      --- trac-0.9.5-ja-1/trac/web/auth.py 2006-01-09 16:48:09.000000000 +0900
      +++ trac-0.9.5-ja-1kai/trac/web/auth.py 2006-06-18 01:54:37.000000000 +0900
      @@ -64,7 +64,8 @@
           def get_navigation_items(self, req):
               if req.authname and req.authname != 'anonymous':
                   yield ('metanav', 'login', '%s としてログイン中' % req.authname)
      -            yield ('metanav', 'logout',
      +            if not self.config.getbool('trac' , 'must_login'):
      +                yield ('metanav', 'logout',
                          Markup('<a href="%s">ログアウト</a>' 
                                 % escape(self.env.href.logout())))
               else:
      url: archives/2006/06/trac-2.html --- trac-0.9.5-ja-1/trac/web/standalone.py 2006-01-09 16:48:09.000000000 +0900
      +++ trac-0.9.5-ja-1kai/trac/web/standalone.py 2006-06-18 01:16:14.000000000 +0900
      @@ -288,7 +288,8 @@
                   return
       
               req.remote_user = None
      -        if path_info == '/login':
      +        if path_info == '/login' or \
      +           env.config.getbool('trac' , 'must_login') :
                   auth = self.server.auths.get(project_name) or \
                          self.server.auths.get('*')
                   if not auth:
      常にログインした状態で利用させるのでログアウトは選択できないようにした(選択しても、またログイン状態になるので意味がない)
    2. trac.iniの[trac]セクションに設定を追加する。
      [trac]
       :
      must_login = true