TracGanttCalendarプラグイン をカスタマイズ(Trac 0.10)

会社で運用しているTracのバージョンはいまだに 0.10 です。
この記事も、Trac 0.10とTracGanttCalendarプラグインの0.10 brancheについてのものです。

svn checkout http://svn.sourceforge.jp/svnroot/shibuya-trac/plugins/ganttcalendarplugin/branches/0.10

ガントチャートが出ると、便利です!

超有用なこのプラグインですが、次のような不満があったりします。。

  1. チケットの並び順が...
    • チケット番号順でもない(?)です。できれば開始日で並べ替えると、前後関係が把握しやすくて吉ですよね
  2. 画面レイアウトの関係で、チケットの概要の文字数が多いと、省略されてしまい、一覧性に乏しい
    • このガントをプリントして、打ち合わせに使いたい時に困る。せっかくチケットを入れているのだから、この情報以外にExcelでガントを作りたくない!(ただ、年間計画とかの大きな粒度のスケジュールはExcelで作っていたりしますが・・・)
  3. プリントできない(消える)
    • TracのデフォルトのCSSが印刷時にForm要素を問答無用でdisplay:noneなので困ります


これらの不満を解消すべく、カスタマイズしてみました。
カスタマイズのポイント

  1. チケットの並び順は、開始日の古いもの順
  2. ガントチャートに、チケットタイトルを省略せずに表示する。(ついでにチケットへのリンクやHover、開始、終了日を表示する)
    • 多少表示はごちゃごちゃしてしまいますが、一覧性を優先した結果です。
  3. プリント可能に
    • TracCSSにある、Form.printableFormを使えるようにクラス名を付与する
  4. (追加で)デフォルトのソート対象をコンポーネントから、マイルストーンに変更
    • うちの環境では、この方が都合がよいです

カスタマイズ後の画面表示

  • ソートが開始日順となり、ガントチャートのバーの部分にチケットタイトルが表示される


  • チケットタイトルのHover


カスタマイズ内容

Index: ganttcalendar/ticketgantt.py
===================================================================
--- ganttcalendar/ticketgantt.py	(リビジョン 429)
+++ ganttcalendar/ticketgantt.py	(作業コピー)
@@ -52,7 +52,7 @@
         show_closed_ticket = req.args.get('show_closed_ticket')
         sorted_field = req.args.get('sorted_field')
         if sorted_field == None:
-           sorted_field = 'component'
+           sorted_field = 'milestone'
 
         if baseday != None:
            r = re.match(r'^(\d+)/(\d+)/(\d+)$', baseday)
@@ -112,22 +112,26 @@
         tickets=[]
         for id, type, summary, owner, description, status, due_assign, due_close, complete, item in cursor:
            due_assign_date = None
+           due_assign_date_short = None #assign date without year
            due_close_date = None
+           due_close_date_short = None #close date without year
            try:
               t = time.strptime(due_assign,"%Y/%m/%d")
               due_assign_date = date(t[0],t[1],t[2])
+              due_assign_date_short = date(t[0],t[1],t[2]).strftime('%m/%d')
            except ValueError, TypeError:
               continue
            try:
               t = time.strptime(due_close,"%Y/%m/%d")
               due_close_date = date(t[0],t[1],t[2])
+              due_close_date_short = date(t[0],t[1],t[2]).strftime('%m/%d')
            except ValueError, TypeError:
               continue
            if item == None or item == "":
               item = "*"
            if complete != None and len(complete)>1 and complete[len(complete)-1]=='%':
               complete = complete[0:len(complete)-1]
-           ticket = {'id':id, 'type':type, 'summary':summary, 'owner':owner, 'description': description, 'status':status, 'due_assign':due_assign_date, 'due_close':due_close_date, 'complete': complete, sorted_field: item}
+           ticket = {'id':id, 'type':type, 'summary':summary, 'owner':owner, 'description': description, 'status':status, 'due_assign':due_assign_date, 'due_close':due_close_date, 'complete': complete, sorted_field: item, 'due_assign_short':due_assign_date_short, 'due_close_short':due_close_date_short}
            self.log.debug(ticket)
            tickets.append(ticket)
 
@@ -233,6 +237,8 @@
                        'url':url, 'short_summary':t['summary'][0:10],
                        'assign':assign, 'todow':todow, 'latew':latew, 'complete':complete
                       })
+            # add for sort by assign date.
+            ts.sort(cmp=lambda x, y: cmp(x['assign'], y['assign']))
 
         req.hdf['gan'] = {
             'weekdays':[u"月", u"火", u"水", u"木", u"金", u"土", u"日"],
Index: ganttcalendar/templates/gantt.cs
===================================================================
--- ganttcalendar/templates/gantt.cs	(リビジョン 429)
+++ ganttcalendar/templates/gantt.cs	(作業コピー)
@@ -119,7 +119,7 @@
 }
 </style>
 
-<form>
+<form class="printableform">
 <table class="list">
   <tr>
     <td>
@@ -324,7 +324,18 @@
         <?cs if:t.complete > 0 && t.assign != -1 ?>
         <div style="top: <?cs var:offset * ti + 60 ?>px; left: <?cs var:t.assign * dw ?>px; width: <?cs var:t.complete ?>px;" class="ticket ticket_done"></div>
         <?cs /if ?>
-
+        <!-- add for showing ticket title on gantt bar -->
+        <div class="ticket" style="position:absolute; height:8px; top: <?cs var:offset * ti + 48 ?>px; left: <?cs if:t.assign * dw < 0 ?>3<?cs else ?><?cs var:t.assign * dw ?><?cs /if ?>px; width: 100%; font-size:10px; color:black">
+            <span class="tip">
+              <pre><span class="type"><?cs var:t.ticket['type'] ?></span>#<?cs var:t.ticket['id'] ?>: <?cs var:t.ticket['summary'] ?></pre>
+              <strong>担当者</strong>: <?cs var:t.ticket['owner'] ?><br />
+              <strong>開始日</strong>: <?cs var:t.ticket['due_assign'] ?><br />
+              <strong>終了日</strong>: <?cs var:t.ticket['due_close'] ?><br />
+              <strong>達成率</strong>: <?cs var:t.ticket['complete'] ?>%<br />
+              <strong>詳細</strong>: <pre><?cs var:t.ticket['description'] ?></pre>
+            </span>
+            <a href="<?cs var:t.url ?>" target="_blank">#<?cs var:t.ticket['id'] ?>:<?cs var:t.ticket['summary'] ?> (<?cs var:t.ticket['due_assign_short'] ?> - <?cs var:t.ticket['due_close_short'] ?>)</a>
+        </div>
 <?cs set:offset = offset + 1 ?>
 
       </span>

パッチをソースに適用しつつ、次のような感じでやります

cd <チェックアウト先の0.10ディレクトリ>
patch -p0 < gantt.patch
python setup.py bdist_egg
cp -f dist/TracGanttCalendarPlugin-0.0.1-py2.4.egg /usr/share/trac/plugins/
service httpd restart

Windows環境(Trac Lighting)なら、
http://cetus.sakura.ne.jp/softlab/toolbox1/index.html#difpat
とかのpatchツールを使うといいかもしれません。
また、
C:\TracLight\python\share\trac\plugins
あたりに、出来上がったeggファイルをコピーすればよいです。

ついでに

レポート用のSQLです。
マイルストーン別に 開始日か終了日が設定されていないチケットをリストアップします

SELECT p.value AS __color__,
  milestone AS __group__,
  id AS ticket, summary, status, assigned.value AS 開始日, closed.value AS 終了日, t.type AS type,
  (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
  time AS created

 FROM
   ticket t
   LEFT JOIN enum p
     ON p.name = t.priority AND p.type = 'priority'

  LEFT JOIN
   (select ticket, name, value from ticket_custom where name = 'due_assign' ) assigned
    ON t.id = assigned.ticket
  LEFT JOIN
   (select ticket, name, value from ticket_custom where name = 'due_close' ) closed
    ON t.id = closed.ticket

 WHERE
   (assigned.value IS NULL)
     OR
   (assigned.value = '')
     OR
   (closed.value IS NULL)
     OR
   (closed.value = '')

 ORDER BY (milestone IS NULL),milestone, t.id

運用

私は次のように使っています。

  1. 要件定義書から、大きな粒度のチケットを切り出す。要件定義書のすべての要件を出し切る。
  2. 要件単位に大まかなスケジュールを組む
    • 開始日と終了日が空っぽのものを上記レポート用SQLで一覧にして、その一覧から大まかなスケジュールをいったん作ってしまいます。
  3. これでガントチャートにチケットが表示されるようになるので、スケジュールの調整をガントを見ながら行う
  4. 案ができたら、ガントをプリントなりプロジェクタに写すなりして確認会を行う