« TracLightningにコバンザメしてKanonと同様にPluginをインストールする | トップページ | Dockerでkanon(Trac)を動かしてみた »

2014年11月 2日 (日)

チケットの表示ページで(ユーザとか)DBからSELECTできるようにするプラグイン

★はじめに
Trac使っていてよく言われていたのが次のに挙げた二つです。いまさらになってしまうんですが作ってみました。ただし、TracのDBの中身とかSQLがわからない人にはちょっと使うのは難しいとおもいます。私自身それほどのレベルではないですが(^_^A;
  • ユーザ名からアカウント名の頭文字がわからん人がいる(ZガンダムがMSZ-006だってすぐにはわからんようなもの?)
  • カスタムフィールドに設定できないようなフィールドをDBのクエリで選択可能にしたい(数が多いがほかのフィールドやユーザでフィルタがかけられるとか)
結局やることはDBから持ってくるってことをプラグイン化すればいいだけですが、SQLを頑張って書けばいろいろなことができます。

★どういうことができる
確認のために下の方につけた設定では次の4つのことができます。
  1. ヘッダのところをDBから値を持ってきて次の行に表示Ticket_db_header_2
  2. チケット変更のところをreadonlyに変更するTicket_db_readonly
  3. チケット変更のところをDBからのQuery結果でSELECTにするTicket_db_field_select
  4. チケットのアクションのところの担当の変更をDBからのQuery結果でSELECTにするTicket_db_owner_select
ユーザ選択の時にマネージャのみを選択できるようにしたり、直前のownerををデフォルトで選択しておくとかの例を載せています。SQLを頑張って書けばいろいろ使えると思います。

★インストール方法
  1. 最後に添付してあるファイルをダウンロードし展開する
  2. そのなかのフォルダでpython setup.py install
  3. 管理画面からプラグインを有効にする。
  4. 次を参考にtrac.iniに設定を追加する。

★設定方法
trac.iniを直接書き換えます。
適切な位置に
[dbqueryfield]
を追加する。
その下の中身のデータはよくある次の形式で書いていきます。
グループ名.データ名 = 値
※ グループ名は適当でかまいませんがフィールド名を入れておくとわかりやすいかも
データ名は次の4つがあります
  1. operation
  2. field
  3. type
  4. sql
※ operationがreadonlyの場合はsqlは設定しません
※ typeは省略するとすべてのtypeに対して処理が行われます
※ typeは一つの値しか設定できません
1 operationに設定できるのは次の4つです
  • readonly フィールドを変更できないように(隣のフィールドのSQLで参照したいときとかに参照される側にreadonlyにする)
  • header ヘッダの表示の次の行にクエリ結果を表示
  • field フィールドをクエリ結果で選択可能に
  • action アクションのオーナ変更を選択できるように
2 fieldは通常はカスタムフィールド名
operationがaction:"アクションのオーナ変更"の場合は"アクション名_次のステータス"になります。わからない場合はソース表示で確認してみてください"id=action_アクション名_次のステータス_owner"となっているところがあるのでその中を抜いていただければOkです。
3 typeを指定した場合はこの処理をするtypeを指定します
"バグ"とか"タスク"とかですね
4 sqlデータを取得するためのSQLを指定する
SQLにはチケットのフィールド名を埋め込むことができます。pythonのフォーマットの%(~~)型のような書き方ですがconfigの値に"%("が含まれるとエラーになるので%を$に置き換えてください。
・operationがheaderの場合はSELECTの結果は表示名一つです。ユーザフィールドの値を元にユーザ名を持ってくる場合は次のようになります
SELECT s.value FROM session_attribute s WHERE s.sid='$(user)s' AND s.name='name'
・operationがfieldまたはactionの場合はSELECTの結果はフィールドの値、表示名、デフォルトかどうか(1:Default)です。ユーザフィールドを選択にする場合のSQLは次のようになります。

SELECT s.value FROM session_attribute s WHERE s.sid='$(user)s' AND s.name='name'

・長いSQLも大丈夫みたいです(これは直前のownerが選択された状態で表示されます)
SELECT s.sid, s.sid || ':' || s.value, '1' FROM session_attribute s WHERE  s.name='name' AND s.sid=(SELECT tc.oldvalue FROM ticket_change tc WHERE tc.ticket='$(id)d' AND tc.field='owner' AND tc.time=(SELECT tc1.time FROM ticket_change tc1 WHERE tc1.ticket='$(id)d' AND field='owner' ORDER BY tc1.time DESC LIMIT 1) ORDER BY tc.time DESC LIMIT 1) UNION SELECT s.sid, s.sid || ':' || s.value, '0' FROM session_attribute s WHERE  s.name='name' AND s.sid<>(SELECT tc.oldvalue FROM ticket_change tc WHERE tc.ticket='$(id)d' AND tc.field='owner' AND tc.time=(SELECT tc1.time FROM ticket_change tc1 WHERE tc1.ticket='$(id)d' AND field='owner' ORDER BY tc1.time DESC LIMIT 1) ORDER BY tc.time DESC LIMIT 1)

★サンプルの設定
trac.iniのこのプラグインの設定
[dbqueryfield]
manager_assign_reassign.field = manager_assign_reassign
manager_assign_reassign.operation = action
manager_assign_reassign.sql = SELECT s.sid, s.sid || ':' || s1.value, 0 FROM session_attribute s LEFT JOIN session_attribute s1 on s.sid=s1.sid AND s1.name='name' WHERE s.authenticated='1' AND s.name='emp_type' AND s.value='1'
reassign_reassign.field = reassign_reassign
reassign_reassign.operation = action
reassign_reassign.sql = SELECT s.sid, s.sid || ':' || s.value, 0 FROM session_attribute s WHERE s.authenticated='1' AND s.name='name'
reject_reassign.field = reject_reassign
reject_reassign.operation = action
reject_reassign.sql = SELECT s.sid, s.sid || ':' || s.value, '1' FROM session_attribute s WHERE  s.name='name' AND s.sid=(SELECT tc.oldvalue FROM ticket_change tc WHERE tc.ticket='$(id)d' AND tc.field='owner' AND tc.time=(SELECT tc1.time FROM ticket_change tc1 WHERE tc1.ticket='$(id)d' AND field='owner' ORDER BY tc1.time DESC LIMIT 1) ORDER BY tc.time DESC LIMIT 1) UNION SELECT s.sid, s.sid || ':' || s.value, '0' FROM session_attribute s WHERE  s.name='name' AND s.sid<>(SELECT tc.oldvalue FROM ticket_change tc WHERE tc.ticket='$(id)d' AND tc.field='owner' AND tc.time=(SELECT tc1.time FROM ticket_change tc1 WHERE tc1.ticket='$(id)d' AND field='owner' ORDER BY tc1.time DESC LIMIT 1) ORDER BY tc.time DESC LIMIT 1)
test.field = test
test.operation = readonly
type.field = type
type.operation = readonly
user_f.field = user
user_f.operation = field
user_f.sql = SELECT s.sid, s.sid || ':' || s.value, CASE WHEN s.sid='$(user)s' THEN '1' ELSE '0' END FROM session_attribute s WHERE s.name='name'
user_f.type = タスク
user_h.field = user
user_h.operation = header
user_h.sql = SELECT s.value FROM session_attribute s WHERE s.sid='$(user)s' AND s.name='name'
trac.iniのカスタムフィールドはユーザの選択を確認できるように追加
[ticket-custom]
complete = text
complete.label = 進捗率(%)
complete.order = 2
due_assign = text
due_assign.date = true
due_assign.date_empty = on
due_assign.label = 開始予定日
due_assign.order = 0
due_close = text
due_close.date = true
due_close.date_empty = on
due_close.label = 終了予定日
due_close.order = 1
parents = text
parents.label = 親チケット
user = text
user.format = plain
user.label = ユーザ
user.options =
user.order = 4
user.value =
trac.iniのワークフローのところは"承認者へ回送","差し戻し","担当者変更"の三つを作うぃ少し簡単にするように修正
[ticket-workflow]
leave = * -> *
leave.default = 1
leave.name = 変更しない
leave.operations = leave_status
manager_assign = assigned -> manager_assigned
manager_assign.name = 承認者へ回送
manager_assign.operations = set_owner
manager_assign.permissions = TICKET_MODIFY
reassign = new,assigned,reopened -> assigned
reassign.name = 担当者変更
reassign.operations = set_owner
reassign.permissions = TICKET_MODIFY
reject = manager_assigned -> assigned
reject.name = 差し戻し
reject.operations = set_owner
reject.permissions = TICKET_MODIFY
reopen = closed -> reopened
reopen.name = 差し戻す
reopen.operations = del_resolution
reopen.permissions = TICKET_CREATE
resolve = manager_assigned -> closed
resolve.name = 承認する
resolve.operations = set_resolution
resolve.permissions = TICKET_MODIFY
resolve.set_resolution = 対応済
resolve2 = assigned -> closed
resolve2.name = 対応せずに解決にする
resolve2.operations = set_resolution
resolve2.permissions = TICKET_MODIFY
resolve2.set_resolution = 不正,対応しない,重複,再現しない
ユーザプロファイルにemp_typeを作って偉い人は1を設定する
Sqlite_database_browser_2
プラグインの本体ticket_filter.py(ライセンスはBSD)を挙げておきます
# -*- coding: utf-8 -*-
from genshi.builder import tag
from genshi.filters.transform import Transformer
from trac.core import Component
from trac.core import implements
from trac.web.api import IRequestFilter
from trac.web.api import ITemplateStreamFilter
class TicketDBQueryFieldFilter(Component):
    """
    チケット表示画面でDBにqueryして表示を変更するfilter
    """
    implements(IRequestFilter, ITemplateStreamFilter)
    FIELDS_INFO = {}
    def __init__(self):
        options = self.config.options('dbqueryfield')
        for _option, _value in options:
            self.log.debug("option = ")
            o = _option.split('.')
            self.log.debug(o)
            field = o[0]
            option = o[1]
            try:
                opt = self.FIELDS_INFO[field]
            except:
                self.FIELDS_INFO[field]={}
                opt = self.FIELDS_INFO[field]
                opt['type'] = ''
            opt[option] = _value.replace("$(","%(")
        for key in self.FIELDS_INFO.keys():
            v = self.FIELDS_INFO[key]
            operation = v['operation']
            k = v['field']
            if operation=='readonly': #ReadOnlyにする
                v['id'] = 'field-%s' % k
                v['name'] = 'field_%s' % k
                if v['field'] in ('type', 'priority', 'milestone', 'component', 'version', 'severity'):
                    v['xpath'] = '//select[@id="field-%s"]' % k
                else:
                    v['xpath'] = '//input[@id="field-%s"]' % k
            elif operation=='header':
                v['xpath'] = '//td[@headers="h_%s"]/text()' % k
            elif operation=='field':
                v['id'] = 'field-%s'
                v['name'] = 'field_%s' % k
                v['xpath'] = '//input[@id="field-%s"]' % k
            elif operation=='action':
                v['id'] = 'action_%s_owner' % k
                v['name'] = 'action_%s_owner' % k
                v['xpath'] = '//input[@id="action_%s_owner"]' % k
                v['operation'] = 'field'
            else:
                self.log.error('Unknown operation(%s).' % operation)
    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        return handler
    def post_process_request(self, req, template, data, content_type):
        if req.path_info.startswith('/ticket/'):
            field_values = []
            tkt = data['ticket']
            tkt['id'] = tkt.id
            tkt['authuser'] = req.authname
            cursor = self.env.get_db_cnx().cursor()
            for key in self.FIELDS_INFO.keys():
                v = self.FIELDS_INFO[key]
                if v['type'] and v['type'] != tkt['type']:
                    continue
                operation = v['operation']
                k = v['field']
                try:
                    field_value = tkt[k]
                except:
                    field_value = ''
                value = None
                xpath = v['xpath']
                if operation=='readonly':
                    value = tag.input(id=v['id'], name=v['name'], value=tkt[k], readonly=1)
                elif operation=='header':
                    if tkt[k]: #カスタムフィールドに値が入っている場合
                        sel_data = []
                        cursor.execute(v['sql'] % tkt)
                        sel_data.append(field_value)
                        for row in cursor:
                            sel_data.append(tag.br())
                            sel_data.append(row[0])
                            break
                        value = tag.span(*sel_data)
                elif operation=='field':
                    sel_data = []
                    cursor.execute(v['sql'] % tkt)
                    options=[]
                    for row in cursor:
                        if str(row[2]) == '1':
                            options.append(tag.option(row[1], value = row[0], selected = True))
                        else:
                            options.append(tag.option(row[1], value = row[0]))
                    value = tag.select(options, id=v['id'], name=v['name'])
                if value:
                    field_values.append({'key':k, 'value':value, 'xpath':xpath})
            data['filter'] = field_values
        return template, data, content_type
    # ITemplateStreamFilter methods
    def filter_stream(self, req, method, filename, stream, data):
        if 'filter' in data:
            for r in data['filter']:
                path = r['xpath']
                stream |= Transformer(path).replace(r['value'])
        return stream

|

« TracLightningにコバンザメしてKanonと同様にPluginをインストールする | トップページ | Dockerでkanon(Trac)を動かしてみた »

コメント

似たようなことが AutocompleteUsersPlugin でもできます。
http://trac-hacks.org/wiki/AutocompleteUsersPlugin

最新のバージョンで以下の2つの機能拡張が含まれています。
良かったら試してみてください。

http://trac-hacks.org/ticket/8477
http://trac-hacks.org/ticket/11415

投稿: t2y | 2014年11月16日 (日) 23時17分

t2yさん情報ありがとうございます。
ふつうな使い方のところではAutocompleteUsersPlugin使えそうですね。

投稿: u-z | 2014年11月17日 (月) 21時27分

いつも参照させていただいております。
私は、今SDWBのチケットを設計しています。ユーザ名の日本語表示のところで悩やまされています。
ご提供させているプラグインを適用したら、以下のエラーが出ています。
Traceback (most recent call last):
File "C:\SDWorkbenchServer\python\lib\site-packages\trac-1.0-py2.7.egg\trac\web\api.py", line 504, in send_error
data, 'text/html')
File "C:\SDWorkbenchServer\python\lib\site-packages\trac-1.0-py2.7.egg\trac\web\chrome.py", line 967, in render_template
if self.stream_filters:
File "C:\SDWorkbenchServer\python\lib\site-packages\trac-1.0-py2.7.egg\trac\core.py", line 78, in extensions
components = [component.compmgr[cls] for cls in classes]
File "C:\SDWorkbenchServer\python\lib\site-packages\trac-1.0-py2.7.egg\trac\core.py", line 199, in __getitem__
component = cls(self)
File "C:\SDWorkbenchServer\python\lib\site-packages\trac-1.0-py2.7.egg\trac\core.py", line 138, in __call__
self.__init__()
File "build\bdist.win32\egg\ticketdbqueryfieldplugin\ticket_filter.py", line 23, in __init__
option = o[1]
IndexError: list index out of range

pythonは全く初心者で、わからないですが、
option=o[1]で何を取ろうとするかは、理解していませんので、
ぜひご教授いただきたいとおもいますが、よろしくお願いします。

options = self.config.options('dbqueryfield')
for _option, _value in options:
self.log.debug("option = ")
o = _option.split('.')
self.log.debug(o)
field = o[0]
option = o[1]

投稿: 林 | 2014年12月15日 (月) 13時25分

林さん。エラーチェックとか全くしてなくてすみません。
エラーの発生場所は、trac.iniに[dbqueryfield]のなか
のconfigを読み込んで'='の左側の文字列を'.'がある
ところでsplitして配列にして、その1番目の要素にアクセス
しているところです。o[1]でエラーになるんだと'.'がない
とかそういったtrac.iniの間違いだと思います。

たとえば次の場合は
manager_assign_reassign.field = manager_assign_reassign

つぎのように処理されていきます。
_option <= "manager_assign_reassign.field" # configの'='より左
_value <= "manager_assign_reassign" # configの'='より右
o <= ["manager_assign_reassign", "field"] # _optionを'.'で分割

'.'があればo[1]にアクセスしてエラーにはならないはずです。

投稿: u-z | 2014年12月15日 (月) 23時51分

コメントを書く



(ウェブ上には掲載しません)


コメントは記事投稿者が公開するまで表示されません。



トラックバック


この記事へのトラックバック一覧です: チケットの表示ページで(ユーザとか)DBからSELECTできるようにするプラグイン:

« TracLightningにコバンザメしてKanonと同様にPluginをインストールする | トップページ | Dockerでkanon(Trac)を動かしてみた »