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これ、もうちょっといろいろ手を加えたい。
tracプラグインはtrac-hacks.orgにいろいろ登録されている。今回作ったプラグインも、ここに登録することにしよう。
t-h.o (trac-hacks.org) にアカウントがなければ、アカウント登録ページに必要な情報を入力することで簡単に登録できる。ユーザ用のWikiページができるので、アカウント名はアルファベットにしておくのがいい。
プラグイン名は、そのままトップページの一覧に載るので、ラクダ表記なWikiName、つまり大文字から始まって大文字を2文字以上含む文字列にしておくのがいい。えーと。QueryStatusHelperはちょっといけてないので、クエリページのUIをアシストするから、QueryUIAssistPlugin にしよう。プラグイン名は末尾をPluginにする。
Page titleはトップページの一覧にも載るので重要。簡潔に内容を表すように書く。
プレビューして違和感がなければ、いよいよ、登録っ!
登録しちゃったー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].
説明をもりもり追加する。
Web観点のテストなので、twill か selenium が使えるはず。 JQueryが絡むので、twillだとちょっと荷が重いかな。seleniumを使うことにする。
selenium の構造はこんな感じ。テストコードから Selenium Server を経由してWebブラウザを操作したり Assert する。
http://seleniumhq.org/download/ から Selenium Server と Internet Explorer Driver Server を入手して、jarファイルをダブルクリックして実行しておく。
> "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|dcheckboxtest.py を右クリックして、Run as - python unit test で実行してやって、グリーンを確認する。OK。
*1 このコード動作させるのにすごく苦労した。time.sleep(2)が重要。
気になってたことはクリアしたので、いよいよtrac-Hacks.orgで公開しよう。
体調不良で二日ほど穴をあけてしまったが、前回ひとまず完成したので、公開にむけたタスクを消化していく。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 くらいでどうだろうか。ちょっと範囲が広い気がするけれども、さっきのよりかはずっといい。
先人いわく、自分のソフトウェアをオープンソースとしてリリースしたいのであれば、
これに従い、ライセンスを選択していこう。tracは今日時点では modified BSD licenseを選択しているので、これを踏襲するのがよいだろう。あるいはさらに「名前を借りてはならない」という制限がなくなった 2-clause BSD Licenseや MIT License、特許で訴えないことを表明している Apache License 2.0 などを選択するのもいい。GPLとの同梱を意識して、MIT or GPL のデュアルライセンスである例もよくみかける。
先人の教えに従い、分類情報をPyPIのリストに従って追加する。Programming Language, License, OS, Development Status, Framework くらいをつけておけばいいかな。
守るべきは、まずはTracのスタイル。すなわち、
あらら。javascriptの名前つき関数の作法*1JavaScript: the Good Parts のそれ*2 と矛盾する。自分のポリシーには反するけれど、コミュニティのポリシーに合わせておくべきなので、ここは素直に直すことにしよう。
少なくとも、setup.py の description, クラスのDOCSTRING はつけておく。tracのadminページと、我らがDeveloperPluginのAPI機能で表示してもらえるので。
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',
],
},
)
昨日は、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;
次回は、公開にむけていろいろ整備しよう。
↑ syo [ご活用いただきましてありがとうございます! 私が普段利用している環境では特に新たな要望もあがってこないなどの理由で..]