今日の三角巾(さんかくきん) by matobaa

| プロフィール | 欲しい物リスト | おとなりページ | @matobaa
追記

2012-12-23 [長年日記]

TicketCountTableマクロをちょっと改造

TicketCountTableが便利そうだったので使ってみて、改造してみた。
qの部分にカスタムフィールドを使えるように。

--- C:/Users/matobaa/Downloads/TicketCountTable-r191/TicketCountTable/ticketcounttable/TicketCountTable.py.org	 12 23 22:47:44 2012
+++ C:/Users/matobaa/Downloads/TicketCountTable-r191/TicketCountTable/ticketcounttable/TicketCountTable.py	 12 23 22:58:14 2012
@@ -114,7 +114,7 @@
             query += " left outer join ticket_custom as cy on cy.ticket=id and cy.name='%s'" % yaxiscolname
         
         # 条件文及びgroup値を追加
-        query += " where %s group by ifnull(%s,'')" % (condition, yaxiskey)
+        query += " %s group by ifnull(%s,'')" % (condition, yaxiskey)
         query += " order by 1"
 
         #self.env.log.info(query)
@@ -239,11 +239,12 @@
                 # DBからデータを取得する
                 if custom:
                     # カスタムフィールドの場合
-                    query = u"select distinct ifnull(value,'') as v from ticket t left outer join ticket_custom c on c.ticket=t.id on c.name='%s' where %s order by 1" % (axiscolname, condition)
+                    query = u"select distinct ifnull(value,'') as v from ticket t left outer join ticket_custom c on c.ticket=t.id on c.name='%s' %s order by 1" % (axiscolname, condition)
                 else:
                     # それ以外の場合
-                    query = u"select distinct ifnull(%s,'') as v from ticket t where %s order by 1" % (axiskey, condition)
+                    query = u"select distinct ifnull(%s,'') as v from ticket t %s order by 1" % (axiskey, condition)
 
+                self.env.log.debug(query)
                 axis = []
                 cursor = db.cursor()
                 cursor.execute(query)
@@ -255,11 +256,15 @@
     # int型のものに対しても文字型と同様に''で囲んでいるが、SQLiteなら大丈夫のはず。
     def createcondition(self, params):
         conditions = None
+        join = ''
         condition = '('
         if params.has_key('q'):
             conditions = params['q'].split('&')
             for cond in conditions:
                 p = cond.split('=')
+                if len([p[0] for field in TicketSystem(self.env).custom_fields if field['name'] == p[0]]) > 0:
+                    join += " left outer join ticket_custom as %s on %s.ticket=id and %s.name='%s' " % (p[0],p[0],p[0],p[0])
+                    p[0] += '.value'
                 if len(p) == 2:
                     if condition != '(':
                         condition += ') AND ('
@@ -269,6 +274,8 @@
                         orstrs.append("%s='%s'" % (unicode(p[0]), unicode(orcond)))
                     orvalues = ' OR '.join(orstrs)
                     condition += orvalues
+        else:
+            condition += '1=1'
         condition += ')'
-        return condition
+        return join + ' where ' + condition
これ、もうちょっといろいろ手を加えたい。

本日のツッコミ(全1件) [ツッコミを入れる]

syo [ご活用いただきましてありがとうございます! 私が普段利用している環境では特に新たな要望もあがってこないなどの理由で..]


2012-12-19 [長年日記]

気になることは全部やっつけたので、いよいよ登録する。

tracプラグインはtrac-hacks.orgにいろいろ登録されている。今回作ったプラグインも、ここに登録することにしよう。

まずはユーザ登録

t-h.o (trac-hacks.org) にアカウントがなければ、アカウント登録ページに必要な情報を入力することで簡単に登録できる。ユーザ用のWikiページができるので、アカウント名はアルファベットにしておくのがいい。

ユーザ登録したら、プラグインの登録

プラグイン名は、そのままトップページの一覧に載るので、ラクダ表記なWikiName、つまり大文字から始まって大文字を2文字以上含む文字列にしておくのがいい。えーと。QueryStatusHelperはちょっといけてないので、クエリページのUIをアシストするから、QueryUIAssistPlugin にしよう。プラグイン名は末尾をPluginにする。

Page titleはトップページの一覧にも載るので重要。簡潔に内容を表すように書く。

  • WikiName of hack: QueryUiAssistPlugin
  • Page title: toggle checkboxes on double-click it's label
  • description: At query when double-click a checkbox clears neighbour checkboxes and check it. double-click a label at left of checkboxes filps checked or not.
  • example: None
  • classification: Plugin, 0.12, 1.0, query

プレビューして違和感がなければ、いよいよ、登録っ!

Register a new Hack

Created wiki page.
Created SVN layout.
Added SVN write
permission.
Finished.

Hack Details

The Subversion repository path for 0.12 is http://trac-hacks.org/svn/queryuiassistplugin/0.12.
The Subversion repository path for 1.0 is http://trac-hacks.org/svn/queryuiassistplugin/1.0.
The page for your hack is QueryUiAssistPlugin.

登録しちゃったー

コミットする

登録すると、subversionの特定のパスへのコミット権がもらえるので、指定されたパスに作ったプラグインをコミットする。

指定されたリンクをクリックすると空っぽである。このURLの頭にtsvn:をつけると TortoiseSVNが起動してくるので、開発している Workspace\trac\QueryStatusHelerPluginフォルダにいったん上書きチェックアウトしてからコミットすると簡単。

コミットログは、「プラグイン名: 説明」と書く。こうすることで、コミットログ中のWikiNameからWikiページへジャンプできるようになる。以降、コミットログにはプラグイン名のWikiNameを書くようにする。

コミットした。[12451].

Wikiページを編集する。

説明をもりもり追加する。


2012-12-18 [長年日記]

そういえばテスト書いてないじゃん。

Web観点のテストなので、twill か selenium が使えるはず。 JQueryが絡むので、twillだとちょっと荷が重いかな。seleniumを使うことにする。

selenium の構造はこんな感じ。テストコードから Selenium Server を経由してWebブラウザを操作したり Assert する。

http://seleniumhq.org/download/ から Selenium Server と Internet Explorer Driver Server を入手して、jarファイルをダブルクリックして実行しておく。

Python クライアントドライバは、easy_install で導入できる:

> "C:\Program Files\BitNami\python\Scripts\easy_install.exe" selenium
Searching for selenium
Reading http://pypi.python.org/simple/selenium/
Reading http://code.google.com/p/selenium/
Reading http://www.openqa.org/
Reading http://seleniumhq.org/
Best match: selenium 2.28.0
Downloading http://pypi.python.org/packages/source/s/selenium/selenium-2.28.0.tar.gz#md5=fd253a2ea94fe14cc5f3e8328cda7a53
Processing selenium-2.28.0.tar.gz
Running selenium-2.28.0\setup.py -q bdist_egg --dist-dir c:\users\matobaa\appdata\local\temp\easy_install-lzspr6\selenium-2.28.0\egg-dist-tmp-gmwns0
C:\PROGRA~1\BitNami\python\lib\distutils\dist.py:267: UserWarning: Unknown distribution option: 'src_root'
  warnings.warn(msg)
warning: no files found matching 'docs\api\py\index.rst'
Adding selenium 2.28.0 to easy-install.pth file
    
Installed c:\progra~1\bitnami\python\lib\site-packages\selenium-2.28.0-py2.7.egg
    
Processing dependencies for selenium
Finished processing dependencies for selenium
>

テストコードを書く

PyDev Package Explorer で QueryStatusHelperPlugin を選択した状態から、Ctrl-N で、Python Module を作る。uiassist.testsパッケージに、Templateは Unittestを選択して、checkboxtest という名前でモジュールを作る。

で、これにソースをがりがり書いていく。こんな感じ:

from selenium import selenium
import time
import unittest
    
class Test(unittest.TestCase):
    def setUp(self):
        self.verificationErrors = []
        self.selenium = selenium("localhost", 4444, "*chrome", "http://localhost:8080/1.0/")
        self.selenium.start()
    
    def test_double_click_field_from_uri(self):
        sel = self.selenium
        sel.open("query?status=!closed")
        sel.double_click("label_0_status")
        self.failUnless(not sel.is_checked("_0_status_accepted"))
        self.failUnless(not sel.is_checked("_0_status_assigned"))
        self.failUnless(sel.is_checked("_0_status_closed"))
        self.failUnless(not sel.is_checked("_0_status_new"))
        self.failUnless(not sel.is_checked("_0_status_reopened"))
    
    def test_double_click_field_added_by_adder(self):
        sel = self.selenium
        sel.open("query?status=!closed")
        time.sleep(2)*1
        sel.select("add_filter_0", "Vote")
        sel.double_click("label_0_vote")
        self.failUnless(sel.is_checked("0_vote_a"))
        self.failUnless(sel.is_checked("0_vote_b"))
        self.failUnless(sel.is_checked("0_vote_c"))
        self.failUnless(sel.is_checked("0_vote_d"))
        # regression
        sel.double_click("label_0_status")
        self.failUnless(not sel.is_checked("_0_status_accepted"))
        self.failUnless(not sel.is_checked("_0_status_assigned"))
        self.failUnless(sel.is_checked("_0_status_closed"))
        self.failUnless(not sel.is_checked("_0_status_new"))
        self.failUnless(not sel.is_checked("_0_status_reopened"))
    
    def test_double_click_field_added_by_adder_repeatly(self):
        sel = self.selenium
        sel.open("query?status=!closed")
        time.sleep(2)
        sel.select("add_filter_0", "Vote")
        sel.double_click("label_0_vote")
        sel.uncheck("0_vote_c")
        sel.double_click("label_0_vote")
        self.failUnless(not sel.is_checked("0_vote_a"))
        self.failUnless(not sel.is_checked("0_vote_b"))
        self.failUnless(sel.is_checked("0_vote_c"))
        self.failUnless(not sel.is_checked("0_vote_d"))
    
    def test_double_click_on_checkbox(self):
        sel = self.selenium
        sel.open("query?status=!closed")
        sel.double_click("_0_status_accepted")
        self.failUnless(sel.is_checked("_0_status_accepted"))
        self.failUnless(not sel.is_checked("_0_status_assigned"))
        self.failUnless(not sel.is_checked("_0_status_closed"))
        self.failUnless(not sel.is_checked("_0_status_new"))
        self.failUnless(not sel.is_checked("_0_status_reopened"))
    
    def test_double_click_on_checkbox_label(self):
        sel = self.selenium
        sel.open("query?status=!closed")
        sel.double_click("//label[@for='_0_status_accepted']")
        self.failUnless(sel.is_checked("_0_status_accepted"))
        self.failUnless(not sel.is_checked("_0_status_assigned"))
        self.failUnless(not sel.is_checked("_0_status_closed"))
        self.failUnless(not sel.is_checked("_0_status_new"))
        self.failUnless(not sel.is_checked("_0_status_reopened"))
    
    def test_double_click_on_checkbox_label_added_by_adder(self):
        sel = self.selenium
        sel.open("query?status=!closed")
        time.sleep(2)
        sel.select("add_filter_0", "Vote")
        sel.double_click("//label[@for='0_vote_a']")
        sel.double_click("//label[@for='0_vote_c']")
        self.failUnless(not sel.is_checked("0_vote_a"))
        self.failUnless(not sel.is_checked("0_vote_b"))
        self.failUnless(sel.is_checked("0_vote_c"))
        self.failUnless(not sel.is_checked("0_vote_d"))
    
    def tearDown(self):
        self.selenium.stop()
        self.assertEqual([], self.verificationErrors)

trac.iniはこんな感じ:

[ticket-custom]
vote = radio
vote.options = a|b|c|d
checkboxtest.py を右クリックして、Run as - python unit test で実行してやって、グリーンを確認する。OK。

*1 このコード動作させるのにすごく苦労した。time.sleep(2)が重要。

次回は

気になってたことはクリアしたので、いよいよtrac-Hacks.orgで公開しよう


2012-12-17 [長年日記]

前回の続き

体調不良で二日ほど穴をあけてしまったが、前回ひとまず完成したので、公開にむけたタスクを消化していく。Dive Into Pythonとか、setup スクリプトを書くとかを参考にしながら。

パッケージ名・モジュール名

作り始めた当初はとりあえず作り始めてしまったので、プロジェクト名はtrac、プラグイン名/パッケージ名/モジュール名/クラス名は適当すぎるQueryStatusHelperPlugin, query, doubleclick, Checkbox になっている。これら、公開するにあたり不自然ではないか?

パッケージ名は、実行環境のほかのモジュールから、from __package__ import __module__ で一意に特定して参照できる必要がある。つまり、このままだとfrom query import doubleclickである。これではちょっとおこがましい。http://docs.python.jp/2/py-modindex.html に掲載されて、おいおい違うよ、と言われない程度の名前がいい。

そこで、from uiassist import query くらいでどうだろうか。ちょっと範囲が広い気がするけれども、さっきのよりかはずっといい。

ライセンス

先人いわく、自分のソフトウェアをオープンソースとしてリリースしたいのであれば、

  1. 独自のライセンスを書いてはならない。
  2. 独自のライセンスを書いてはならない。
  3. 独自のライセンスを書いてはならない。
  4. ライセンスがGPLである必要はないが、GPL互換である必要がある。
  5. 独自のライセンスを書いてはならない。
と教えている。

これに従い、ライセンスを選択していこう。tracは今日時点では modified BSD licenseを選択しているので、これを踏襲するのがよいだろう。あるいはさらに「名前を借りてはならない」という制限がなくなった 2-clause BSD LicenseMIT License、特許で訴えないことを表明している Apache License 2.0 などを選択するのもいい。GPLとの同梱を意識して、MIT or GPL のデュアルライセンスである例もよくみかける。

分類

先人の教えに従い、分類情報をPyPIのリストに従って追加する。Programming Language, License, OS, Development Status, Framework くらいをつけておけばいいかな。

コーディングスタイル

守るべきは、まずはTracのスタイル。すなわち、

  • (not) have long lines
  • (not) have multiple statements per line
  • (not) break the naming conventions
および PEP 8 (日本語) あたり。

あらら。javascriptの名前つき関数の作法*1JavaScript: the Good Parts のそれ*2 と矛盾する。自分のポリシーには反するけれど、コミュニティのポリシーに合わせておくべきなので、ここは素直に直すことにしよう。

*1 function fooBar() {...}

*2 var fooBar = function() {...}

ドキュメンテーション

少なくとも、setup.py の description, クラスのDOCSTRING はつけておく。tracのadminページと、我らがDeveloperPluginのAPI機能で表示してもらえるので。

反映したsetup.py

from setuptools import setup, find_packages
    
version = '0.1'
    
setup(
    name='QueryStatusHelper',
    version=version,
    classifiers=[
                  "Programming Language :: Python",
                  "Programming Language :: Python :: 2.6",
                  "Framework :: Trac",
                  "Operating System :: OS Independent",
                  "Development Status :: 3 - Alpha",
                  "Intended Audience :: Developers",
                  "Intended Audience :: Information Technology",
                  "License :: OSI Approved :: BSD License",
                  "Natural Language :: Japanese",
                  "Topic :: Software Development :: Bug Tracking",
                  ],
    license='Modified BSD',
    author='MATOBA Akihiro',
    author_email='moc.liamg@skcah-cart+aabotam',
    url='http://trac-hacks.org/wiki/matobaa',
    description='On Query page, flip checkbox status on double click it\'s label',
    install_requires=['Trac >= 0.12'],
    packages=find_packages(exclude=['*.tests*']),
    package_data={
        'uiassist': ['htdocs/*/*'],
    },
    entry_points={
        'trac.plugins': [
            'querystatushelper = uiassist.query',
        ],
    },
)

次回は

そろそろ t-h.o に登録しようか。どうしようか


2012-12-14 [長年日記]

昨日は、statusとvoteについて、ラベルをダブルクリックしたらチェックボックスが反転するところまで実装できた。こんなかんじ:

jQuery(document).ready(function() {
    handler = function(event) {
        field = this.id.slice(6)
        jQuery('input[name="' + field + '"]').each(function() {
            checked = jQuery(this).attr("checked")
            jQuery(this).attr("checked", !checked)
        })
    }
    checkboxes = jQuery("#filters input[type='checkbox']");
    fields = []
    for (i in checkboxes) {
        fields[checkboxes[i].name] = true;
    }
    for (field in fields) {
        jQuery('#label_' + field).dblclick(handler)
    }
});
今日はその続き。そうそう、jQueryって書くの面倒だから$って書くことにする。

クエリ条件は Add の次のドロップダウンを選ぶことで追加できるんだけど、当然ながら、追加したチェックボックスでも同じようにダブルクリックを処理できることが期待されるはず。だけど、今の実装では、Document.ready 時点で存在するチェックボックスに機能追加してるので、後から追加した条件では機能しなかった。今日はそれをまず直す。Add でクエリ条件を追加したら、それをトリガーに、追加したクエリ条件のチェックボックスのラベルに dblclick をバインドしたい。

IEのF12開発ツールで Ctrl-B でドロップダウンを選択してみると、id=add_filter_0 という名前の要素が見つかった。こいつのイベント処理は、query.js (126) にあった:

126: $("#filters select[name^=add_filter_]").change(function(event) {
 :
171: var control = createCheckbox(propertyName, option,
172:                              propertyName + "_" + option);
同じように onchangeのときに、我がQueryStatusHelperPluginで dblclickイベントに handler をバインドしてやればいいはず。こんなかんじ:
$("#filters select[name^=add_filter_]").change(binder)
…… と思ったんだけど、残念ながらこっちの処理のほうが早くて、query.js側のonchangeの後ではなくて、直前にバインドされてしまって、処理したいときには追加すべきチェックボックスがまだ存在しない、という状況になってしまった。

じゃー。こっちの onchange は setTimeout してやればいいんじゃないか。こんなかんじ:

setTimeout(function(){ $("#filters select[name^=add_filter_]").change(binder)}, 1000)
…… と思ったんだけど、実装してみたら、確かに query.js 側のonchangeの後に処理されるようになったのだが、query.js側のchangeハンドラがイベントの selectIndex をクリアしてしまっていて、こっちが処理したいときには、新たに追加されたチェックボックスの名前がわからない、という状況になってしまった。

しかたないので、document.ready の時と同じように、見つかった全部のチェックボックスに dblclickハンドラを追加しちゃえー。こんなかんじ:

setTimeout(function(){ $("#filters select[name^=add_filter_]").change(function() {
   $('#label_' + field).dblclick(handler);
}) });
……と思ったんだけど、残念、反転処理が偶数回追加されちゃうと、元に戻っちゃう、という状況になってしまった。

しかたないので、見つかった全部のチェックボックスでいったん dblclickハンドラがあれば外して、あらためて全部にdblclickハンドラを追加する、という処理にしてみた。こんな感じ:

$('#label_' + field).unbind('dblclick', handler).dblclick(handler)

できた、できた。

整形すると、結局、こんな感じになった:
QueryStatusHelperPlugin/query/htdocs/js/enabler.js

(function($) {
    // event source であるラベル の  id と一致するチェックボックスの状態を反転する
    var flip = function(event) {
        field = this.id.slice(6)
        $('input[name="' + field + '"]').each(function() {
            checked = $(this).attr("checked")
            $(this).attr("checked", !checked)
        })
    }
    // event source であるチェックボックスと同名のチェックボックスをすべてクリアし、sourceだけをチェックする
    var selectone = function(event) {
        $('input[name="' + this.name + '"]').attr('checked', false);
        $(this).attr('checked', 'checked');
    }
    // ページ内にある検索条件のチェックボックスにselectoneを、それらを束ねるラベルに flip をバインドする。
    // すでにflipがバインドされている可能性があるので、一度外してみてから再バインドする。
    var binder = function() {
        checkboxes = $("#filters input[type='checkbox']");
        for (i in checkboxes) {
            $('#label_' + checkboxes[i].name).unbind('dblclick', flip).dblclick(flip)
        }
        checkboxes.dblclick(selectone);
    };
    $(document).ready(function() {
        // On Ready
        binder();
        // On Change
        setTimeout(function() {
            $("#filters select[name^=add_filter_]").change(binder)
        }, 1000);
    })
})(jQuery);

なお参考までに、いまバインドされてるchangeハンドラはこんな感じでとれた:

orgonchange = $($("#filters select[name^=add_filter_]")[0]).data('events')['change'][0].handler;


ツッコミお待ちしております。 半期 四半期 全カテゴリ