Hatena::ブログ(Diary)

アジャイルSEを目指すブログ

2010-10-02

Shibuya.trac 第8回勉強会でLTしました。

LT時は時間内に全く終わらず、すごく残念な感じに...orz

折角作った資料ですので、SlideShareアップロードしてみました。

ちょっと概要説明が多くて、コードに関する実践的な部分が少なかったかも。

2010-05-20

PythonでUTCからJSTに変換する方法

TracXML-RPCで取得した時刻がUTCなので、これをJSTに変換する方法を見つけたので、メモ。

参考ページ

- どうすればUTCにPythonでのローカル時間を変換するのですか?

ソース

>>> import xml_rpc
>>> server = xml_rpc.XmlRpcServer('http://admin:admin@localhost/trac/TracTest/lo
gin/xmlrpc')
>>> ticket_1 = server.ticket.get(1)
>>> print ticket_1
[1, <DateTime '20100321T18:20:48' at f74d50>, <DateTime '20100321T18:20:48' at f
74d78>, {'status': 'new', 'description': 'bvabafda', 'reporter': 'sinsoku', 'cc'
: '', 'component': u'\u305d\u306e\u4ed6', 'owner': 'somebody', 'summary': 'aaaaa
aaaa', 'priority': u'\u666e\u901a', 'keywords': '', 'version': '', 'milestone':
'', 'due_assign': '', 'type': u'\u30bf\u30b9\u30af', 'due_close': '', 'complete'
: ''}]
>>> import time
>>> utc_date = time.strptime(ticket_1[2].value,'%Y%m%dT%H:%M:%S')
>>> print utc_date
(2010, 3, 21, 18, 20, 48, 6, 80, -1)
>>> import calendar
>>> jst_date = time.localtime(calendar.timegm(utc_date))
>>> print jst_date
(2010, 3, 22, 3, 20, 48, 0, 81, 0)

インタラクティブシェルでやった内容をそのままコピペ

重要なのはこの辺ですね。

>>> print utc_date
(2010, 3, 21, 18, 20, 48, 6, 80, -1)
>>> import calendar
>>> jst_date = time.localtime(calendar.timegm(utc_date))
>>> print jst_date
(2010, 3, 22, 3, 20, 48, 0, 81, 0)

シーケンスをエポック秒に直して、それをJSTに変換しています。

関数の簡単な説明は下記参照。

calendar.timegm(t(シーケンス))

timeモジュールで利用するシーケンスに相当するエポック秒からの経過時間を取得する。

time.localtime([secs(エポック秒からの経過時間])

ローカルのタイムゾーンを考慮した現在時刻のシーケンスを返す。

2010-04-12

TracLightningでQueryChartPluginをデフォルト設定にする

QueryChartPluginはチケットの進捗をバーンダウンチャートで表示出来るプラグインです。

TracLightiningだと最初からインストール済みなのですが、毎回初期設定が面倒なので、

プロジェクトを作成したらすぐに使えるように設定してみました。

Plugin作者の方のページ

参考ページ

設定

TracLightの下記ファイルにQueryChartPluginに必要な設定をする。*1

下記のファイルはTracLight 2.5.0α4でのtrac.ini.defaultでの変更箇所です。

変更箇所はunified形式です。

  • /TracLight/install/trac.ini.default
--- trac.ini.default.bak
+++ trac.ini.default
@@ -35,6 +35,12 @@
 complete = text
 complete.label = 進捗率(%)
 complete.order = 3
+last_assigned = text
+last_assigned.label = 作業開始日
+last_assigned.order = 4
+last_closed = text
+last_closed.label = 作業完了日
+last_closed.order = 5
 
 [decorator]
 calendar_fields=due_assign,due_close
@@ -59,4 +65,6 @@
 src=site/logo.png
 width = -1
 
+[querychart]
+order = assigned:last_assigned, accepted,reopened, closed:last_closed

要は、初期設定で必要な

  • カスタムフィールド
  • querychartの設定値

をTracLightningが使用している初期設定のファイルに突っ込むだけです。

これで、次回から

create-project HogeHogeProject

とかで新規プロジェクトを作成してやると、最初からQueryChartPluginの準備が完了した状態になります。

他にも全プロジェクトで適用したいワークフローなどを突っ込んでみるのも便利かもしれませんね。

*1trac.ini.defaultはTracLight 2.4.1より前はSJISのため、UTF-8に変更する必要があります。

2010-03-22

TracのWiki編集画面でプレビューを表示するTrac Plugin

TracのWiki編集画面で、「プレビュー」を押して画面遷移するのが面倒じゃないですか?*1

f:id:sinsoku:20100321215946p:image

これ。

Wiki全体の変更内容を見るより、自分が変更した部分だけサクッと見たい。

という事で、TracPluginを作ってみました。

参考にしたプラグイン

とても参考にさせて頂きました。

こういう参考になるコードが簡単に見られるっていい時代ですよね。

WikiPreviewOverrayPlugin

入力したテキストを選択して、右クリックを押すことでWiki編集画面から画面遷移せずにWiki変更後のイメージを表示出来ます。*2

こんな感じ。

f:id:sinsoku:20100321221826p:image

内部の話ですが、技術的には

  • Python
  • Javascript(jQuery)
  • Ajax

辺りを使用しています。

Python初心者+Javascript未経験だったため、良い勉強になりました。

インストール方法

WindowsXP+TracLightning2.4.0での確認しかしていませんので、動かなかったらスミマセン。

まずは、後述しているソースを下記フォルダ構成で配置する。

  • フォルダ構成
■WikiPreviewOverrayPlugin
 setup.py
 ■wikipreviewoverray
  __init__.py
  wikipreviewoverray.py
  ■htdocs
   wikipreviewoverray.css
  ■templates
   wikipreviewoverray.js

後は、下記のどれかで動かしてみることができます。

  • リンクだけ作って確認してみる方法
python setup.py develop
  • 普通にinstallする方法
python setup.py install
  • eggを作って、Tracのweb画面からinstallする方法
python setup.py bdist_egg

作成されたeggを管理画面からインストールする。

f:id:sinsoku:20100321231529p:image


インストール後、もしかしたらサーバの再起動が必要かもしれません。

ソース

外部に置くリポジトリがないため、ひとまずブログ上に公開。

  • setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from setuptools import find_packages, setup

setup(
    name='WikiPreviewOverrayPlugin',
    version='0.1',
    packages=find_packages(exclude=['*.tests*']),
    package_data={'wikipreviewoverray': [ 'htdocs/*', 'templates/*']},
    entry_points = """
        [trac.plugins]
        WikiPreviewOverray = wikipreviewoverray
    """,
    url='http://d.hatena.ne.jp/sinsoku/',
    author = 'sinsoku',
    author_email = "sinsoku.listy@gmail.com",
    description = u"Wiki Preview",
    license ="New BSD",
)
  • /wikipreviewoverray/__init__.py
# -*- coding: utf-8 -*-
from wikipreviewoverray import *
  • /wikipreviewoverray/wikipreviewoverray.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import re

from trac.core import *
from trac.web.chrome import ITemplateProvider, add_stylesheet, add_script
from trac.web.api import IRequestFilter, IRequestHandler
from trac.util import escape, Markup
from trac.perm import IPermissionRequestor
from trac.wiki.formatter import wiki_to_html
from pkg_resources import resource_filename

class WikiPreviewOverrayPlugin(Component):
    implements(IRequestHandler, ITemplateProvider, IRequestFilter)

    # ITemplateProvider methods
    def get_templates_dirs(self):
        yield resource_filename(__name__, 'templates')

    def get_htdocs_dirs(self):
        yield 'wikipreviewoverray', resource_filename(__name__, 'htdocs')

    # IRequestHandler methods
    def match_request(self, req):
        return re.match(r'^/WikiPreviewOverray(?:(.*))', req.path_info) is not None


    def process_request(self, req):
        if re.match(r'^/WikiPreviewOverray/wikipreviewoverray.js',req.path_info) :
            if 'WIKI_CREATE' in req.perm('wiki') or 'WIKI_ADMIN' in req.perm('wiki'):
                return 'wikipreviewoverray.js',{},'text/plain'

    # IRequestFilter methods
    def post_process_request(self, req, template, data, content_type) :
        if re.match(r'/wiki/', req.path_info) and req.method =='GET' and req.args.get('action') == "edit" :
            add_script(req, '/WikiPreviewOverray/wikipreviewoverray.js')
        if not re.match(r'^wikipreviewoverray/wikipreviewoverray.css',req.path_info) :
            add_stylesheet(req, 'wikipreviewoverray/wikipreviewoverray.css')

        if re.match(r'^/WikiPreviewOverray/post',req.path_info) :
            if req.method == 'POST' :
                text = req.args.get('tohtml')
                html = unicode(wiki_to_html(text, self.env, req, absurls=1))
                
                req.send_response(200)
                req.send_header('Content-Type', 'applicatiion/x-www-form-urlencoded')
                req.end_headers()
                req.write(html)

        return template, data, content_type

    def pre_process_request(self, req, handler):
        return handler
  • /wikipreviewoverray/htdocs/wikipreviewoverray.css
div#WikiPreviewOverray_Container{
  position:absolute;
  padding:5px;
  background-color: #FFFFFF;
  z-index: 999;
  color: #000000;
  border: 1px solid #000000;
  -moz-border-radius: 10px; /* for Fx */
  -webkit-border-radius: 10px; /* for Safari */
  -border-radius:10px;
}
div#WikiPreviewOverray_Container span#WikiPreviewOverray_Button{
  width:5px;
  height:5px;
  padding: 0px;
  display: block;
  float: right;
  border: 1px solid #000000;
  -moz-border-radius: 9px; /* for Fx */
  -webkit-border-radius: 9px; /* for Safari */
  -border-radius:9px;
}
div#WikiPreviewOverray_Container span#WikiPreviewOverray_Button:hover{
  background-color:#FF0000;
}
  • /wikipreviewoverray/templates/wikipreviewoverray.js
$( function() {
    $(document).ready(function(){
        $("div#footer").after("<div id='WikiPreviewOverray_Container'/>")
        $("div#WikiPreviewOverray_Container")
            .css( "max-width", 380)
            .css( "max-height", 480)
            .css( "top" ,  0)
            .css( "left",  0);
        $("#WikiPreviewOverray_Container").append("<span id='WikiPreviewOverray_Button'></span>")
        $("span#WikiPreviewOverray_Button").click( function(){
            $("#WikiPreviewOverray_Container").hide("slow");
        });
        $("#WikiPreviewOverray_Container").hide();
        $("#WikiPreviewOverray_Container").append("<div id='WikiPreviewOverray_Html'/>")
        $("div#WikiPreviewOverray_Html")
            .css( "overflow", "auto")
            .css( "max-width", 370)
            .css( "max-height", 470)
    });

    $(document).bind("contextmenu",function(event){ 
        if(document.selection) {
            // IE
            var range = document.selection.createRange();
            var selected_value = range.text;
        } else {
            // IE 以外
            var org = document.getElementById("text");
            var start = org.selectionStart;
            var end = org.selectionEnd;
            var selected_value = org.value.substring(start, end);
        }

        if(selected_value.length > 0) {
            var url = location.pathname.split("/");
            var req_url = "";
            // /wiki/WikiStartの分をurlから削って、req_urlを構築する
            for(var i = 1; i < url.length - 2; i++)
                req_url += "/" + url[i];
            req_url += "/WikiPreviewOverray/post";
            
            $.post(
                req_url,
                {"tohtml" : selected_value, "__FORM_TOKEN" : $('[name=__FORM_TOKEN]').attr('value')},
                function(data, status) {
                    $("div#WikiPreviewOverray_Html").html(data);
                    $("div#WikiPreviewOverray_Container")
                        .css( "top" ,  event.pageY)
                        .css( "left",  event.pageX);
                    $("#WikiPreviewOverray_Container").hide();
                    $("#WikiPreviewOverray_Container").show("slow");
                },
                "html"
            );
            selected_value = "";
        }
        
        // 右クリックメニュー(コンテキストメニュー)を非表示にする。
        return false;
    });
})

Plugin開発者向け

今回のPlugin作成で、POSTを使用する所でハマったので、備忘録を残しておきます。

Tracの本体側ではCSRF対策として、フォームにハッシュのような文字列を付加しています。

本体(/TracLight/python-lib/trac/trac/web/main.py)のソースだと、190行目付近。

  • main.py
# Protect against CSRF attacks: we validate the form token for
# all POST requests with a content-type corresponding to form
# submissions
if req.method == 'POST':
    ctype = req.get_header('Content-Type')
    if ctype:
        ctype, options = cgi.parse_header(ctype)
    if ctype in ('application/x-www-form-urlencoded',
                 'multipart/form-data') and \
            req.args.get('__FORM_TOKEN') != req.form_token:
        raise HTTPBadRequest('Missing or invalid form token. '
                             'Do you have cookies enabled?')

上記を見ていただければわかるように、

  • 'Content-Type'が'application/x-www-form-urlencoded'もしくは'multipart/form-data'でない
  • '__FORM_TOKEN'がない

と400 Bad Requestが発生してしまいます。

これを回避するためには、Tracのページから'__FORM_TOKEN'を取得し、POSTに含めてやります。

$.post(
  req_url,
  {"__FORM_TOKEN" : $('[name=__FORM_TOKEN]').attr('value')
  },
  function(data, status) {
    //何かの処理
  },
  "html"
);

気づけば簡単に修正出来ますが、中々Web上に情報がなく、苦戦しました。

追記

ソースコード一式をzipにしてDropboxに置きました。

WikiPreviewOverrayPlugin-0.1.zip

追記2

ソースコードをShibuya.tracのsvnリポジトリにコミットしました。

http://svn.sourceforge.jp/svnroot/shibuya-trac/plugins/WikiPreviewOverrayPlugin/tags/0.1

*1:wysiwyg使えば解決しそうですが、慣れるとテキスト入力の方が早いのですよね・・・

*2:デメリットとして、wiki編集画面で右クリックが使用不可になってしまいます。代用の方法を考えて修正したいですが・・・

2009-12-23

DropboxにTracLightningをインストールしてみた

Tracを外出先でも使うためにDropboxにTracを入れてみました。*1

使用したバージョン

Dropboxのアカウント

Dropbox 0.7.79.exeを実行して、適当に英語の指示に従えばおk。

「私はアカウント持ってません」的なの選んで、メールアドレスとパスワード入れればアカウントは作れます。

TracLightningのインストール

Dropboxをインストールすると、デフォルトなら"My Documents"の下に"My Dropbox"のフォルダが出来る。

ここにTracLightningをインストールする訳だが、そのままインストールすると後々面倒なので、substを使う。

subst T: "C:\Documents and Settings\<user>\My Documents\My Dropbox"

これで"T:\"に"My Dropbox"をマウント出来る。

この状態で、"T:\TracLight"にTracLightningをインストールする。

再起動を促す画面が出るが、再起動せずに終了する。

TracLightningの起動

  1. T:\TracLihgtの下にstartmenuフォルダを作る。
  2. スタートメニューに作成されたショートカットをT:\TracLihgt\startmenuに移動する。
  3. startmenuに下記のDropboxTracStart.bat(必要ならDropboxTracEnd.batも)を作る。
    • DropboxTracStart.bat
subst T: ../..
set MAVEN_HOME=T:\TracLight\maven
set OPENSSL_CONF=T:\TracLight\CollabNetSVN\httpd\conf\openssl.cnf
set PYTHONHOME=T:\TracLight\python
set APR_ICONV_PATH=T:\TracLight\CollabNetSVN\httpd\bin\iconv
set PYTHONPATH=T:\TracLight\python\DLLs\;T:\TracLight\python\Lib;T:\TracLight\python\Lib\plat-win;T:\TracLight\python\Lib\lib-tk;T:\TracLight\Lib\site-packages
set TRACPATH=T:\TracLight\python;T:\TracLight\python\Scripts;T:\TracLight\CollabNetSVN;T:\TracLight\CollabNetSVN\httpd\bin;T:\TracLight\Graphviz\bin;T:\TracLight\maven\bin;T:\TracLight\bin
set PATH=%TRACPATH%;%PATH%

cd /d T:\TracLight
start.bat -e DEBUG
    • DropboxTracEnd.bat
subst T: /d

あとはDropboxTracStart.batを実行すればTracLightningが起動します。


これで、週末はスタバでコーヒー飲みながらサブPCで作業して、帰宅後にネットに繋げば自動的に同期されるようになる。

TracにGitpluginを入れれば、SVNのリポジトリをcloneして持ってきて、自分で弄った後にpushとかも出来て面白いかも。

追記

既存のTracProjectをDropboxに入れるなら、下記2点を修正・実行する。

  • Trac.ini
    • パスをT:\TracLight\〜に修正する。
  • SVNリポジトリの更新
    • "trac-admin T:\TracLight\trac\<project> resync"のコマンドを実行する。

*1:無線LAN契約すれば済む話ですが・・・