2009/11/15

Tracチケット一括取り込み

ウォータフォールな現場の開発中期や、アジャイルな現場でのストーリー抽出後に
一括してチケットを登録したい場合がある。
当然Tracにはこうした要望を叶えるインターフェースがある。
以下、チケットのインポート処理の流れ。
  1. インポートメニューから取り込み画面へ遷移する


  2. 対応しているチケット情報を必要なだけコピーする


  3. コピーした情報をExcelに各種フィールドカラムとしてヘッダに貼り付ける。
    次レコード以降は、チケット内容となる。
    又、取り込み画面の説明に従って「ticket」フィールドを追加する。


  4. 記入後、適当な場所に保存し、取り込み画面でプレビュー表示する


    下記の通り、未記入の項目は初期値で補完される。
    尚、日付型としてExcelフィールドを定義して開始/終了予定日を
    記入しても化ける。
    これらは「文字列」として記入しなければならない。
    又、Billableフィールドは「ON」等とする。


  5. プレビュー内容で問題なければ取り込み実施する。
    実施後は、取り込み結果が表示される。


  6. 取り込んだチケットを実際に確認する。
    報告者は取り込み時に指定しなかった場合は、取り込み実施者が割り当てられている。


2009/11/14

Tracで簡易WBS(2)

ややあって、より可視化したWBSを作りたくなった。
希望機能は、
  • 進捗と実績差の妥当性の把握
  • チケットの遅延の把握
を踏まえ、改造する。


改造されたファイル群

UbuntuでGWT

Google App EngineがJavaを対応して暫くたったのを期に、
私的アプリを構築したいと思う。
まずは第一歩として、開発環境構築する。
希望開発環境は、
  • Ubuntu内で動くこと
  • どうせならGWTを使いたい
  • Javaなら、やっぱりEclipseでしょ?
そんな訳で、とりあえず現状環境(amd64)で構築する。
※以下、旧GAE pluginを利用する場合。現在は、amd64でも利用できる!
色々試したものの、結局Google謹製プラグインが64bitだと対応してなかった...orz

方針転換して、
  • x86で開発する
  • とはいえ、VirtualBoxやKVMといった仮想環境は面倒
ということで、「chroot」で構築する。
「chroot」、「debootstrap」をインストールする。
後は、
debootstrap --arch i386 karmic karmic_i386 http://ubuntutym.u-toyama.ac.jp/ubuntu/

等と実行して基礎を作る。
本来であれば、次に親環境の設定を色々とコピーするべきところは割愛。
chrootし、
/etc/apt/source.lst
に「multiverse」、「universe」、「restricted」以下を加え、
  apt-get update & apt-get install eclipse
等と実行する。
...が、ca-certificates-javaがライブラリを解決出来ず失敗(>_<)
面倒だが、ld.so.conf末尾にパスを追加し、
  dpkg --confgiure -a
を実施して完了させる。

やっとEclipseがインストール出来たので、早速起動!
甘かった...procがマウントされていないと起動できないと吐かれてストールした。
申し付けに従って、テキトーにマウントするように設定するとめでたく走った(^-^)

必要なプラグインをインストールし、
Google App Engine for Javaを使ってみよう!を参考に、
サンプルプロジェクトを走らせてみる。
と、又も引っかかった。
どうやら、libstdc++5が必要とのこと。
ln -s libstdc++.so.6 libstdc++.so.5
等と浅はかにするもダメだったので、インストールすることにした。
しかし、karmicではサポート外?らしく、Jauntyから持って来てインストールした。
ここまでやって、何とか開発環境が整った。

2009/11/12

MSBuild (1)

Trac経由によるHudsonの利用を目指して、
まずはビルド環境の基礎となるMSBuildに取り組む。
MSBuild自体は、dotNetが入っている環境であれば大体インストール済み。
但し、参照パスが通っていない為、適宜設定しなければならない。
一般的なパスは、
 %windir%\Microsoft.NET
以下の任意バージョン各個となる。
執筆時点では、v3.5が妥当?


参照パス設定後、簡単なプロジェクトで試す。
尚、本来は細かな設定をXML記述で表現すべきところは割愛する。
まず、slnファイル配置ディレクトリに移動する。
複数プロジェクトが別々のディレクトリであっても、
slnファイルがそれらを纏めてくれている。
この為、簡単なビルドであれば「msbuild」と実行するだけでOK。






C:\TDDTestGUI\TDDTestGUI>msbuild
Microsoft (R) Build Engine Version 3.5.30729.1
[Microsoft .NET Framework, Version 2.0.50727.3053]
Copyright (C) Microsoft Corporation 2007. All rights reserved.

2009/11/12 15:54:45 にビルドを開始しました。
ノード 0 上のプロジェクト "C:\TDDTestGUI\TDDTestGUI\TDDTestGUI.sln" (既定のター
ゲット)。
  ソリューション構成 "Debug|Any CPU" をビルドしています。
プロジェクト "C:\TDDTestGUI\TDDTestGUI\TDDTestGUI.sln" (1) は、ノード 0 上に "C:
\TDDTestGUI\
TDDTestGUI\TDDTestGUI.vbproj" (2) をビルドしています (既定のターゲット)。
  Processing 0 EDMX files.
  Finished processing 0 EDMX files.
CoreResGen:
  リソース ファイル "My Project\Resources.resx" を "obj\Debug\TDDTestGUI.Resourc
es.resou
  rces" に処理しています。
CopyFilesToOutputDirectory:
  "obj\Debug\TDDTestGUI.exe" から "bin\Debug\TDDTestGUI.exe" へファイルをコピー
しています。
  TDDTestGUI -> C:\TDDTestGUI\TDDTestGUI\bin\Debug\TDDTestGUI.exe
  "obj\Debug\TDDTestGUI.pdb" から "bin\Debug\TDDTestGUI.pdb" へファイルをコピー
しています。
  "obj\Debug\TDDTestGUI.xml" から "bin\Debug\TDDTestGUI.xml" へファイルをコピー
しています。
プロジェクト "C:\TDDTestGUI\TDDTestGUI\TDDTestGUI.vbproj" (既定のターゲット) の
ビルドが完了しました。

プロジェクト "C:\TDDTestGUI\TDDTestGUI\TDDTestGUI.sln" (1) は、ノード 0 上に "C:
\TDDTestGUI\
TestProject\TestProject.vbproj" (3) をビルドしています (既定のターゲット)。
  Processing 0 EDMX files.
  Finished processing 0 EDMX files.
_CopyFilesMarkedCopyLocal:
  "..\..\..\..\..\..\..\Program Files\TestDriven.NET 2.0\NUnit\2.5\net-2.0\fram
  ework\nunit.framework.dll" から "bin\Debug\nunit.framework.dll" へファイルをコ
ピーしています。
  "C:\TDDTestGUI\TDDTestGUI\bin\Debug\TDDTestGUI.exe" から "bin\Debug\TDDTestGUI
.
  exe" へファイルをコピーしています。
  "..\..\..\..\..\..\..\Program Files\TestDriven.NET 2.0\NUnit\2.5\net-2.0\fram
  ework\nunit.framework.xml" から "bin\Debug\nunit.framework.xml" へファイルをコ
ピーしています。
  "C:\TDDTestGUI\TDDTestGUI\bin\Debug\TDDTestGUI.pdb" から "bin\Debug\TDDTestGUI
.
  pdb" へファイルをコピーしています。
  "C:\TDDTestGUI\TDDTestGUI\bin\Debug\TDDTestGUI.xml" から "bin\Debug\TDDTestGUI
.
  xml" へファイルをコピーしています。
CopyFilesToOutputDirectory:
  "obj\Debug\TestProject.dll" から "bin\Debug\TestProject.dll" へファイルをコピ
ーしています。
  TestProject -> C:\TDDTestGUI\TestProject\bin\Debug\TestProject.dll
  "obj\Debug\TestProject.pdb" から "bin\Debug\TestProject.pdb" へファイルをコピ
ーしています。
プロジェクト "C:\TDDTestGUI\TestProject\TestProject.vbproj" (既定のターゲット)
のビルドが完了しました。

プロジェクト "C:\TDDTestGUI\TDDTestGUI\TDDTestGUI.sln" (既定のターゲット) のビル
ドが完了しました。


ビルドに成功しました。
    0 警告
    0 エラー

経過時間 00:00:00.89

※1. ビルドモードがデフォルトで「Debug」である為、「Release」にしたい場合は、
   msbuild /p:Configuration=Release
とプロパティを追加しなければならない。

※2. 出力方法などの指定は、
   msbuild /t:Clean;Build
とターゲットを追加しなければならない。
上記のように複数オプション指定する場合は、「;」で区切る。

2009/10/26

VB6でDoxygen

VB6をレガシーコードからモダンコードに移行させるには、
ユニットテストとコードドキュメント生成が必須となる。
今回は、コードドキュメント生成をDoxygenで行う。
といっても、Doxygen単体ではVB6コード扱えない。
そこでC風コードや、Java風コードに変換しなければならない。
いくつか試した中でも、 vbfilter.py が一番精度が高かったので以後EXEとして利用する。
  1. Doxygenをセットアップ
    本家からセットアップバイナリを落として、セットアップ






    とりあえず、必要なのはDoxywizardのみ。後はお好きにどうぞ。









  2. Doxygenファイルを作成する
    作成には、メニューのDoxywizardを実行する


    プロジェクト概要を設定する。
    設定しておくのは、プロジェクト名くらいかと。


    生成対象ソースコード形式を設定する。
    vbfilterの仕様では、C++に設定する。


    出力フォーマットを設定する。
    出力オプションはお好みでどうぞ。


    グラフ出力等にGraphvizを利用する場合は、設定する。
    ちなみに、Doxygen自体にもグラフ出力機能は同梱されている。


    プロジェクト詳細情報を設定する。
    DOXYFILE_ENCODING値をUTF-8にしないと出力出来ない模様


    bas、cls、frmを追加する。
    ※INPUT_FILTER値には、それぞれvbfilter.exeを設定する

  3. VB6なコードを記述する
    記述仕様:
    • クラス概要は、!@brief
    • メソッド、オブジェクト、変数概要は、*@brief
    • メソッド引数は、*@param
    • 覚書は、@note
    • グラフは、@dot

  4. ドキュメントを生成する
    実行すると、Warning等と進捗がログ表示される
    出力先は、設定ディレクトリ下「html」ディレクトリになる


    プロジェクト名が出力されている


    クラス一覧。クラス概要が出力されている


    クラスメソッド一覧


    概要コメントが出力されている


    グラフ出力、各種詳細コメントが出力されている

2009/10/25

Py2exeでvbfilter.pyをexe化する

pythonicなコードは、一般的な会社等での配布&運用が難しいと思われる。
面倒でもpy2exeを通して、exeとして配布することでMSな人々でも抵抗無く利用出来る。
VB6をDoxygenするのに必要なvbfilterをexe化したメモ。
  1. Pythonをセットアップ
    Pythonicな人はしてあるとは思うけど...何それ?的な人は、PyJUGからどうぞ(^-^)

  2. Py2exeをセットアップ
    http://python.matrix.jp/modules/py2exe.htmlからどうぞ

  3. vbfitlerを用意する
    こちらからどうぞ。Doxygen記事で参考にしました。

  4. setup.pyを作成する

    from distutils.core import setup
    import py2exe
    
    py2exe_options = {
      "compressed": 1,
      "optimize": 2,
      "bundle_files": 2}
    
    setup(
      options = {"py2exe": py2exe_options},
      console = [
        {"script" : "vbfilter.py"}],
      zipfile = None)  
    

  5. buildを実行する
    setup.py py2exe
以上でvbfilter.exeが作成される。
これを利用すれば、ちょっとした作業程度ならダラダラと
dotnetやVBA(VB6含み)の使い捨てコードを書かなくて済む。

[注意点 ]
TracLightning等がインストールされていると、
Python系環境変数はそちらが優先されることが多い。
従って、別途Pythonを追加インストールしてexe作成作業するには、
参照切り替えを行うorライブラリ系参照パスを追加すればよい

Tracで簡易WBS

timingandestimationpluginを導入することで、簡易な工数管理が行える。
しかし、ガントチャートでは工数等が見えない為、WBS的な利用には今一歩といったところ。
そこで、無理矢理ガントチャートへ要りそうな拡張を行う。
とりあえず、欲しい機能は...
  1. 各チケット毎に実績と見積を表示する
  2. チケットの実績と見積のそれぞれの合計を表示する
  3. 実績が見積を超過した場合は、分かりやすく表示する
  4. 進捗率をプログレスバー的な表示以外に、数字として表示する
に絞る。
完成画面:

という訳で、実装する。
当然、編集対象プラグインはganttcalendarになる。
プラグイン構成は、
  • ganntcalendar
    • ticketcalendar.py
    • ticketgannt.py  <- 今回編集する
    • templates
      • gantt.html <- 今回編集する
      • calendar.html
以下、実装後ファイル
[ticketgantt.py]
# -*- coding: utf-8 -*-
import re, calendar, time, sys
from datetime import datetime, date, timedelta
from genshi.builder import tag

from trac.core import *
from trac.web import IRequestHandler
from trac.web.chrome import INavigationContributor, ITemplateProvider
from trac.util.datefmt import to_datetime, format_date, parse_date

class TicketGanttChartPlugin(Component):
    implements(INavigationContributor, IRequestHandler, ITemplateProvider)

    # INavigationContributor methods
    def get_active_navigation_item(self, req):
        return 'ticketgantt'
    
    def get_navigation_items(self, req):
        if req.perm.has_permission('TICKET_VIEW'):
            yield ('mainnav', 'ticketgantt',tag.a(u'ガントチャート', href=req.href.ticketgantt()))

    # IRequestHandler methods
    def match_request(self, req):
        return re.match(r'/ticketgantt(?:_trac)?(?:/.*)?$', req.path_info)

    def adjust( self, x_start, x_end, term):
        if x_start > term or x_end < 0:
            x_start= done_end= None
        else:
            if x_start < 0:
                x_start= 0
            if x_end > term:
                x_end= term
        return x_start, x_end

    def process_request(self, req):
        req.perm.assert_permission('TICKET_VIEW')
        self.log.debug("process_request " + str(globals().get('__file__')))
        ymonth = req.args.get('month')
        yyear = req.args.get('year')
        baseday = req.args.get('baseday')
        selected_milestone = req.args.get('selected_milestone')
        selected_component = req.args.get('selected_component')
        show_my_ticket = req.args.get('show_my_ticket')
        show_closed_ticket = req.args.get('show_closed_ticket')
        sorted_field = req.args.get('sorted_field')
        if sorted_field == None:
            sorted_field = 'component'

        if baseday != None:
            baseday = parse_date( baseday).date()
        else:
            baseday = date.today()

        cday = date.today()
        if not (not ymonth or not yyear):
            cday = date(int(yyear),int(ymonth),1)

        # cal next month
        nmonth = cday.replace(day=1).__add__(timedelta(days=32)).replace(day=1)

        # cal previous month
        pmonth = cday.replace(day=1).__add__(timedelta(days=-1)).replace(day=1)

        first_date= cday.replace(day=1)
        days_term= (first_date.__add__(timedelta(100)).replace(day=1)-first_date).days

        # process ticket
        db = self.env.get_db_cnx()
        cursor = db.cursor();
        sql = ""
        condition=""
        if show_my_ticket == 'on':
            if condition != "":
                condition += " AND "
            condition += "owner ='" + req.authname + "'"
        if show_closed_ticket != 'on':
            if condition != "":
                condition += " AND "
            condition += "status <> 'closed'"
        if selected_milestone != None and selected_milestone !="":
            if condition != "":
                condition += " AND "
            condition += "milestone ='" + selected_milestone +"'"
        if selected_component != None and selected_component !="":
            if condition != "":
                condition += " AND "
            condition += "component ='" + selected_component +"'"

        if condition != "":
            condition = "WHERE " + condition + " "

        sql = ("SELECT id, type, summary, owner, t.description, status, a.value, c.value, cmp.value, milestone, component, etime.value, ttime.value "
                "FROM ticket t "
                "JOIN ticket_custom a ON a.ticket = t.id AND a.name = 'due_assign' "
                "JOIN ticket_custom c ON c.ticket = t.id AND c.name = 'due_close' "
                "JOIN ticket_custom cmp ON cmp.ticket = t.id AND cmp.name = 'complete' "
                "JOIN ticket_custom etime ON etime.ticket = t.id AND etime.name = 'estimatedhours' "
                "JOIN ticket_custom ttime ON ttime.ticket = t.id AND ttime.name = 'totalhours' "
                "%sORDER by %s , a.value ") % (condition, sorted_field)

        self.log.debug(sql)
        cursor.execute(sql)

        tickets=[]
        estimatedhour_sum = 0.0
        totalhour_sum = 0.0
        for id, type, summary, owner, description, status, due_assign, due_close, complete, milestone, component, estimatedhours, totalhours in cursor:
            due_assign_date = None
            due_close_date = None
            try:
                due_assign_date = parse_date(due_assign).date()
            except ( TracError, ValueError, TypeError):
                continue
            try:
                due_close_date = parse_date(due_close).date()
            except ( TracError, ValueError, TypeError):
                continue
            if complete != None and len(complete)>1 and complete[len(complete)-1]=='%':
                complete = complete[0:len(complete)-1]
            try:
                if int(complete) >100:
                    complete = "100"
            except:
                complete = "0"
            complete = int(complete)
            if due_assign_date > due_close_date:
                continue
            if milestone == None or milestone == "":
                milestone = "*"
            if component == None or component == "":
                component = "*"
            if estimatedhours == None or estimatedhours < 0:
                estimatedhours = 0
            else:
                estimatedhours = float(estimatedhours)
            if totalhours == None or totalhours < 0:
                totalhours = 0
            else:
                totalhours = float(totalhours)
            estimatedhour_sum += estimatedhours
            totalhour_sum += totalhours

            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, 
                    'milestone': milestone,'component': component,
                    'estimatedhours':estimatedhours,
                    'totalhours':totalhours}
            #calc chart
            base = (baseday -first_date).days + 1
            done_start= done_end= None
            late_start= late_end= None
            todo_start= todo_end= None
            all_start=(due_assign_date-first_date).days
            all_end=(due_close_date-first_date).days + 1
            done_start= all_start
            done_end= done_start + (all_end - all_start)*int(complete)/100.0
            if all_end <= base < days_term:
                late_start= done_end
                late_end= all_end
            elif done_end <= base < all_end:
                late_start= done_end
                late_end= todo_start= base
                todo_end= all_end
            else:
                todo_start= done_end
                todo_end= all_end
            #
            done_start, done_end= self.adjust(done_start,done_end,days_term)
            late_start, late_end= self.adjust(late_start,late_end,days_term)
            todo_start, todo_end= self.adjust(todo_start,todo_end,days_term)
            all_start, all_end= self.adjust(all_start,all_end,days_term)

            if done_start != None:
                ticket.update({'done_start':done_start,'done_end':done_end})
            if late_start != None:
                ticket.update({'late_start':late_start,'late_end':late_end})
            if todo_start != None:
                ticket.update({'todo_start':todo_start,'todo_end':todo_end})
            if all_start != None:
                ticket.update({'all_start':all_start})

            self.log.debug(ticket)
            tickets.append(ticket)

        # milestones
        milestones = {'':None}
        sql = ("SELECT name, due, completed, description FROM milestone")
        self.log.debug(sql)
        cursor.execute(sql)
        for name, due, completed, description in cursor:
            due_date = to_datetime(due, req.tz).date()
            item = { 'due':due_date, 'completed':completed != 0,'description':description}
            if due==0:
                del item['due']
            milestones.update({name:item})
        # componet
        components = [{}]
        sql = ("SELECT name FROM component")
        self.log.debug(sql)
        cursor.execute(sql)
        for name, in cursor:
            components.append({'name':name})

        holidays = {}
        sql = "SELECT date,description from holiday"
        try:
            cursor.execute(sql)
            for hol_date,hol_desc in cursor:
                holidays[format_date(parse_date(hol_date, tzinfo=req.tz))]= hol_desc
        except:
            pass

        data = {'baseday': baseday, 'current':cday, 'prev':pmonth, 'next':nmonth}
        data.update({'show_my_ticket': show_my_ticket, 'show_closed_ticket': show_closed_ticket, 'sorted_field': sorted_field})
        data.update({'selected_milestone':selected_milestone,'selected_component': selected_component})
        data.update({'tickets':tickets,'milestones':milestones,'components':components})
        data.update({'holidays':holidays,'first_date':first_date,'days_term':days_term})
        data.update({'parse_date':parse_date,'format_date':format_date,'calendar':calendar})
        data.update({'estimatedhour_sum':estimatedhour_sum,'totalhour_sum':totalhour_sum})
        return 'gantt.html', data, None

    def get_templates_dirs(self):
        from pkg_resources import resource_filename
        return [resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        from pkg_resources import resource_filename
        return [('tc', resource_filename(__name__, 'htdocs'))]

[gantt.html]
<が&lt;な所に注意!
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude"
      py:with="px_ti=25;px_hd=46;px_dw=12;px_ch=10;maxtic=len(tickets);">
  <xi:include href="layout.html" />
  <xi:include href="macros.html" />
  <head>
    <script type="text/javascript" src="${chrome.htdocs_location}js/folding.js"></script>
    <script type="text/javascript">
      jQuery(document).ready(function($) {
        $("fieldset legend.foldable").enableFolding(false);
        /* Hide the filters for saved queries. */
        $("#options").toggleClass("collapsed");
      });
    </script>

    <style type="text/css">
      form fieldset.collapsed { 
        border-width: 0px;
        margin-bottom: 0px;
        padding: 0px .5em;
      }
      fieldset legend.foldable :link,
      fieldset legend.foldable :visited { 
        background: url(${chrome.htdocs_location}expanded.png) 0 50% no-repeat;
        border: none;
        color: #666;
        font-size: 110%;
        padding-left: 16px;
      }
      fieldset legend.foldable :link:hover, fieldset legend.foldable :visited:hover {
        background-color: transparent;
      }
      
      fieldset.collapsed legend.foldable :link, fieldset.collapsed legend.foldable :visited { 
        background-image: url(${chrome.htdocs_location}collapsed.png);  
      }
      fieldset.collapsed table, fieldset.collapsed div { display: none }
      table.list     {width:100%;border-collapse: collapse;margin-bottom: 6px;}
      table.list td  {padding:2px;}
      .border_line   {background-color: gray;}
      .hdr           {position: absolute;background-color: #eee;}
      .hdr_title     {display:block;position: absolute; width:100%;text-align:center;font-size:10px;}
      .bdy           {position: absolute;background-color: #fff;text-align: left;top:${px_hd}px;height:${maxtic*px_ti}px;}
      .bdy_elem      {position: absolute;font-size: 9px;left:1px;height:${px_ti-2}px;}

      .tip           {position: static;}
      .tip span.popup{position: absolute;visibility: hidden;background-color: #ffe;color: black;border: 1px solid #555;left: 20px;top: 30px;width: 400px; padding: 3px;}
      .tip:hover span.popup
                     {visibility: visible; z-index: 100;}

      .tic_done      {position: absolute; overflow: hidden; background:lightgreen;}
      .tic_late      {position: absolute; overflow: hidden; background:pink;}
      .tic_todo      {position: absolute; overflow: hidden; background:lightgrey;}
      .tic_done_bl   {position: absolute; overflow: hidden; background:green;}
      .tic_late_bl   {position: absolute; overflow: hidden; background:red;}
      .tic_todo_bl   {position: absolute; overflow: hidden; background:gray;}

      .baseline      {position: absolute; overflow: hidden; border-left: 1px dashed red;}
      .milestone  {position: absolute; overflow: hidden; background-color: red;}
    </style>
  </head>
  <body py:with="weekdays = ['月', '火', '水' ,'木', '金', '土', '日']">
    <form>
      <fieldset id="options">
        <legend class="foldable">設定</legend>
        <table class="list">
          <tr>
            <td>
              基準日<input type="text" id="field-baseday" name="baseday" value="${format_date(parse_date(baseday.isoformat()))}" length="10"/>
            </td>
          </tr>
          <tr>
            <td>
              ソートするフィールド
              <select name="sorted_field">
                <option value="component" selected="${sorted_field=='component' or None}">component</option>
                <option value="milestone" selected="${sorted_field=='milestone' or None}">milestone</option>
              </select><br/>
            </td>
          </tr>
          <tr>
            <td>
              絞込みをします
              マイルストーン = 
              <select name="selected_milestone">
              <py:for each="i in milestones.keys()">
                <option selected="${selected_milestone==i or None}" value="$i">$i</option>
              </py:for>
              </select>
              AND 
              コンポーネント =
              <select name="selected_component">
              <py:for each="i in components">
                  <option selected="${selected_component==i.name or None}" value="${i.name}">${i.name}</option>
              </py:for>
              </select>
            </td>
          </tr>
          <tr>
            <td>
              <input type="checkbox" name="show_my_ticket" checked="$show_my_ticket" />自分のチケットのみ表示
              <input type="checkbox" name="show_closed_ticket" checked="$show_closed_ticket" />closeしたチケットを表示<br/>
  
            </td>
            <td align="right" valign="bottom">
              <input type="submit" value="更新" />
            </td>
          </tr>
        </table>
      </fieldset>
      <table class="list">
        <tr>
          <td>
            <input type="button" value="<<${prev.month}月" onclick="form.year.value = ${prev.year}; form.month.value = ${prev.month}; form.submit();"/>
          </td>
          <td align="center">
            <select name="year">
              <option py:for="y in range(current.year-3,current.year+4)"
                     value="$y"
                     selected="${y==current.year or None}">$y</option>
            </select>
            年
            <select name="month">
              <option py:for="m in [1,2,3,4,5,6,7,8,9,10,11,12]"
                     value="$m" selected="${m==current.month or None}">$m</option>
            </select>
            月
            <input type="submit" value="更新" />
          </td>
          <td align="right">
            <input type="button" value="${next.month}月>>" onclick="form.year.value = ${next.year}; form.month.value = ${next.month}; form.submit();"/>
          </td>
        </tr>
      </table>
    </form>
    <!-- gantt -->
    <div style="position:relative;left:1px;top:1px;width:100%;height:${maxtic*px_ti+px_hd+1+40}px;">
      <!-- right side -->
      <div style="overflow:auto;margin-left:403px;margin-right:4px;position:relative;left:0px;top:1px;height:${maxtic*px_ti+px_hd+1+30}px;">
        <div class="border_line" style="left:0px;top:1px;width:${px_dw*days_term+1}px;height:${maxtic*px_ti+px_hd+1}px;">
          <!-- head and sun,sta,holiday -->
          <div class="bdy" style="position:relative;left:1px;top:${px_hd}px;width:${px_dw*days_term-1}px;height:${maxtic*px_ti}px;"/>
<py:for each="cnt in reversed(range(days_term))" py:with="cur=first_date+timedelta(cnt);wk=cur.weekday()">
          <div py:if="cur.day == 1" py:with="days_thismonth=calendar.monthrange(cur.year,cur.month)[1];" class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*0+1}px;width: ${days_thismonth*px_dw-1}px;height:${(px_hd-4)/3}px;">${cur.year}/${cur.month}</div>
          <div py:if="wk==6 and(cur-first_date).days+7<days_term" class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*1+2}px;width: ${px_dw*7-1}px;height:${(px_hd-4)/3}px;">${cur.month}/${cur.day}</div>
          <div py:if="wk==6 and(cur-first_date).days+7>days_term" class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*1+2}px;width: ${px_dw*(days_term-(cur-first_date).days)-1}px;height:${(px_hd-4)/3}px;"/>
          <div class="hdr hdr_title" style="left:${px_dw*cnt+1}px;top:${(px_hd-4)/3*2+3}px;width: ${px_dw-1}px;height:${(px_hd-4)/3}px;">${weekdays[wk]}</div>
  <py:with vars="holiday_desc = holidays.get(format_date(parse_date(cur.isoformat())));">
    <py:if test="cur.weekday()>4or holiday_desc">
          <div class="border_line" style="position:absolute;top:${px_hd}px; left: ${px_dw*cnt}px; width: ${px_dw+1}px; height: ${maxtic*px_ti+1}px;">
            <div class="hdr" py:attrs="{'title':holiday_desc}" style="top:0px; left:1px; width: ${px_dw-1}px; height: ${maxtic*px_ti}px;"/>
          </div>
    </py:if>
  </py:with>
</py:for>
          <div py:if="first_date.weekday()!=6" class="hdr" style="left:1px;top:${(px_hd-4)/3*1+2}px;width: ${px_dw*(6-first_date.weekday())-1}px;height:${(px_hd-4)/3}px;"/>
          <!-- chart -->
<py:def function="print_chart(kind)">
  <py:with vars="s=tickets[cnt].get('all_start');e=tickets[cnt].get(kind +'_end');">
    <py:if test="e!=None and e-s!= 0">
          <div class="${'tic_'+kind+'_bl'}" style="left:${int(s*px_dw+1)}px;top:${px_ti*cnt+px_hd+((px_ti-px_ch)/2)}px;width: ${int((e-s)*px_dw)}px;height:${px_ch}px;"/>
          <div class="${'tic_'+kind}" style="left:${int(s*px_dw+2)}px;top:${px_ti*cnt+px_hd+((px_ti-px_ch)/2+1)}px;width: ${int((e-s)*px_dw)-2}px;height:${px_ch-2}px;"/>
    </py:if>
  </py:with>
</py:def>
<py:for each="cnt in reversed(range(maxtic))">
          ${print_chart('todo')}
          ${print_chart('late')}
          ${print_chart('done')}
  <py:if test="selected_milestone != '' and selected_milestone != None">
    <py:if test="tickets[cnt].get('milestone')!= None and tickets[cnt].get('milestone') in milestones" py:with="d= milestones[tickets[cnt]['milestone']].get('due')">
      <py:if test="d!=None and 0 <= (d-first_date).days+1 < days_term" py:with="d=(d-first_date).days+1">
          <div class="milestone" style="left: ${d*px_dw}px; top: ${cnt*px_ti+px_hd}px;  width: 2px; height: ${px_ti}px;"></div>
      </py:if>
    </py:if>
  </py:if>
</py:for>
<py:with vars="base = (baseday-first_date).days+1">
          <!-- baseline -->
          <div py:if="base+1 < days_term" class="baseline" style="left:${base*px_dw}px;top:${px_hd}px; height:${maxtic*px_ti}px; width: 0px;"/>
</py:with>
        </div>
      </div>
      <!-- left side -->
      <div style="position:absolute;background-color:gray;left:1px;top:1px;width:402px;height:${maxtic*px_ti+px_hd+1}px;">
        <div class="hdr" style="left:1px;top:1px;width: 89px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">${_(sorted_field)}</span></div>
        <div class="hdr" style="left:91px;top:1px;width:104px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">チケット</span></div>
        <div class="hdr" style="left:196px;top:1px;width:107px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">担当者</span></div>
        <div class="hdr" style="left:304px;top:1px;width:35px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">実績</span></div>
        <div class="hdr" style="left:340px;top:1px;width:35px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">見積</span></div>
        <div class="hdr" style="left:376px;top:1px;width:25px;height:${px_hd-2}px;"><span class="hdr_title" style="top:${(px_hd-2-16)/2}px;">達成</span></div>
<py:def function="print_field(px_x,px_w,ticket_col,dupchk=False)">
        <div class="bdy" style="left:${px_x}px;width:${px_w}px;">
  <py:for each="cnt in range(maxtic)" py:with="t=tickets[cnt]">
    <py:choose>
      <py:when test="ticket_col=='ticket'" py:with="cnt=maxtic-cnt-1;t=tickets[cnt];">
          <div class="bdy_elem" style="top: ${cnt*px_ti}px;width: ${px_w-2}px;">
            <a class="tip" href="${req.href.ticket()}/${t['id']}">#${t['id']}:${t['summary'][0:14]}
              <span class="popup">
                <pre><span class="type">${t['type']}</span>#${t['id']}: ${t['summary']}</pre>
                <strong>担当者</strong>: ${format_author(t['owner'])}<br/>
                <strong>開始日</strong>: ${format_date(parse_date(t['due_assign'].isoformat()))}<br/>
                <strong>終了日</strong>: ${format_date(parse_date(t['due_close'].isoformat()))}<br/>
                <strong>達成率</strong>: ${t['complete']}%<br/>
                <strong>作業実績</strong>: ${t['totalhours']}<br/>
                <strong>予定工数</strong>: ${t['estimatedhours']}<br/>
                <strong>詳細</strong>: <pre>${t['description']}</pre>
              </span>
            </a>
          </div>
      </py:when>
      <py:otherwise>
        <py:choose>
          <py:when test="dupchk">
          <div py:if="not cnt or t[ticket_col]!=tickets[cnt-1][ticket_col]" class="bdy_elem" style="top:${cnt*px_ti}px;width: ${px_w-2}px;">${t[ticket_col]}</div>
          </py:when>
          <py:otherwise>
            <div class="bdy_elem" style="top: ${cnt*px_ti}px;width: ${px_w-2}px;">${ticket_col in ('owner','reporter') and format_author(t[ticket_col]) or t[ticket_col]}</div>
          </py:otherwise>
        </py:choose>
      </py:otherwise>
    </py:choose>
  </py:for>
        </div>
</py:def>
<py:def function="print_time_fields(ttime_x,etime_x,col_width)">
  <py:def function="print_time_value(val,cnt,color='')">
    <div class="bdy_elem" style="top: ${cnt*px_ti}px;width: ${col_width-2}px;${color}">$val</div>
</py:def>
  <py:def function="print_totaltime_value(t,cnt,color='')">
    ${print_time_value('totalhours' in ('owner','reporter') and format_author(t['totalhours']) or t['totalhours'],cnt,color)}
  </py:def>
  <div class="bdy" style="left:${ttime_x}px;width:${col_width}px;">
    <py:for each="cnt in range(maxtic)" py:with="t=tickets[cnt]">
 <py:choose>
   <py:when test="t['totalhours'] &<= t['estimatedhours']">
     ${print_totaltime_value(t,cnt)}
   </py:when>
   <py:otherwise>
     ${print_totaltime_value(t,cnt,'color:red;')}
   </py:otherwise>
 </py:choose>
    </py:for>
    <div class="bdy_elem" style="top: ${maxtic*px_ti}px;width: ${col_width-2}px;">$totalhour_sum</div>
  </div>
  <div class="bdy" style="left:${etime_x}px;width:${col_width}px;">
    <py:for each="cnt in range(maxtic)" py:with="t=tickets[cnt]">
    ${print_time_value('estimatedhours' in ('owner','reporter') and format_author(t['estimatedhours']) or t['estimatedhours'],cnt)}
    </py:for>
    <div class="bdy_elem" style="top: ${maxtic*px_ti}px;width: ${col_width-2}px;">$estimatedhour_sum</div>
  </div>
</py:def>
        ${print_field( 376,25,'complete')}
        ${print_time_fields(304,340,35)}
        ${print_field( 196,107,'owner')}
        ${print_field(  91,104,'ticket')}
        ${print_field(   1, 89,sorted_field,dupchk=True)}
      </div>
    </div>
    <!-- gantt -->
  </body>
</html>

2009/10/19

CodeRush

Visual Studio 2005 時代は、Refactor!がVB.Netにとっての唯一リファクタリングツールだった。
2008時代は、CodeRushがその荷を背負うことになった。
その試食メモ。
  1. まずはセットアップ
    本家からバイナリをダウンロードして、セットアップする
    ちなみに、セットアップ開始前に必ずVisualStudiを終了しなければならない










  2. 早速VisualStudioを起動してみる
    メソッドの横に縦線がメソッド区切りの可視化として表示される


    コンテキストメニューに、「Refactor!」が追加されている...


    試しに実行してみると、「Refactor!」そのものが動く模様
  3. リファクタリングしてみる
    簡単そうな名前変更を試す


    「GetMessage」から「GetHelloMessage」に変更する


    自プロジェクトのみではなく、ソリューション全体がリファクタリング対象
    ということで、TestProjectの方も名前変更されている(^-^)

TestDriven.net

VB6ではIDEと連携したUnitTestフレームワークは一般的ではなく、
最悪、UnitTestって何デスカ?状態だった...
しかし、dotnetに移行することで容易にUnitTestを出来るようになった。
そんな訳で、TestDriven.netのセットアップと運用メモ。
  1. まずはセットアップ
    本家からセットアップバイナリをダウンロードし、セットアップ











  2. 早速使ってみる
    まずは、コンテキストメニューにアイテムが追加されていること確認


  3. テスト対象クラスとメソッドを作成


  4. テスト用プロジェクトを作成し、テストクラスを作成する






  5. テスト対象プロジェクト参照に加える




  6. Nunitライブラリを参照に加える
    TDDをインストールしたディレクトリに配置されている。


  7. テストクラスを実装する
    Message.GetMessage の戻り値が「Hello」であるか検証するコード


  8. テストを走行する
    コンテキストメニューの「Run Test」はカーソル中テストのみを実行する


  9. テスト走行結果を確認する
    コンソールに結果が出力される。
    今回は失敗したので、1 failed


  10. Nunit上でテスト走行させる
    今回は失敗したので、Red






  11. テスト仕様に基づいて、実装コードを修正する


  12. コンテキストから再テスト走行
    今回は成功したので、1 pass


  13. Nunitで再テスト走行
    今回は成功したので、Green
  14. 「3」ないし「7」を繰り返す