概要
Mantisの検索画面およびそこからのExcel出力時に、課題の関連付けで、重複先に設定された課題数を整数値で表示します。
このカスタマイズは、通常用意されているカスタマイズ方法とは逸脱しており、プラグインも使用しません。
すなわち、coreのphpファイルを複数編集し、Mantis全体に影響を及ぼします。
ON・OFF機能も1ありません。
今後のバージョンアップの追従に悪影響を及ぼすと考えてもらっていいと思います。
対象バージョンは1.2.17
となります。
現在開発中の1.3.x
系統とは細部が異なり、同様の変更ができるとは限りません。2
再度触る場合のために調査過程から書いています。
背景(読み飛ばしOK)
各所から上がってくるバグ報告の、改修優先度を決める際の指標の一つとして、同様のバグ報告数を使うとする。
報告は同じバグでも環境ごとに細部がことなるため、先着の課題にコメントで追記していくのは不十分な可能性がある。(加えて重複数を手動でカウントしなければならない(コメント数≠重複数))
そのため、重複の課題登録を許可し、重複が発見された場合は、関連機能を使い、重複元,重複先関係を構築する。
次期改修課題の決定の際は、個々の課題を確認するのではなく、課題のExcel出力から、各課題の概要(タイトル)と優先度(標準機能:別ルールで決定)と報告数(発生数、重複数)を俯瞰し協議する。
関係は標準機能では表示項目に設定できないため、手動でカスタマイズを行い、重複先数のみを集計し、新規列に表示したい。
注意
いろいろな箇所をいじります。
数自体は多くはないのですが、その関連性が見えにくいというか、カスタマイズ中はあっちいったりこっちいったりで混乱します。多分数日経ったら全貌を忘れますし、カスタマイズ中も把握していたというと嘘になります。
プラグインで一から出力機能を作ったほうがまだいい感じに思えます。
できるだけメモをとりながら、なぜここに書いたか、こことこことは似ているのになぜ違うかを把握したほうがいいでしょう。
課題一覧の出力用SQLの確認
「要約」の隣に新たに列を追加します。
この一覧をどうやって作っているのかを追うと、URLの/view_all_bug_page.phpから辿り、/view_all_inc.phpでHTML出力している。しかし、ループする課題の変数$rows
は戻って/view_all_bug_page.phpから
$rows = filter_get_bug_rows( $f_page_number, $t_per_page, $t_page_count, $t_bug_count, null, null, null, true );
で取得されている。
$rows
をデバッグすると、課題情報のオブジェクトが配列で入っているので、DBの取得結果と推測する。
filter_get_bug_rows()
は/core/filter_api.phpの1037行目から長々と書かれているが、SQLの組み立てとだけ認識して末尾から読む。
$t_result = db_query_bound( "$t_select_string $t_from_string $t_join_string $t_where_string $t_order_string", $t_query_clauses['where_values'], $p_per_page, $t_offset );
$t_row_count = db_num_rows( $t_result );
$t_id_array_lastmod = array();
for( $i = 0;$i < $t_row_count;$i++ ) {
$t_row = db_fetch_array( $t_result );
$t_id_array_lastmod[] = (int) $t_row['id'];
$t_rows[] = $t_row;
}
return filter_cache_result( $t_rows, $t_id_array_lastmod );
}
明らかにdb_query_bound()
がクエリ実行なので、引数をデバッグ
var_dump($t_select_string); // "SELECT DISTINCT mantis_bug_table.*"
var_dump($t_from_string); // " FROM mantis_bug_table"
var_dump($t_join_string); // "JOIN mantis_project_table ON mantis_project_table.id = mantis_bug_table.project_id"
var_dump($t_where_string); // "WHERE mantis_project_table.enabled = ? AND ( mantis_bug_table.project_id = 5 ) AND ( ( mantis_bug_table.status in (?, ?, ?, ?, ?) ) ) "
var_dump($t_order_string); // " ORDER BY mantis_bug_table.sticky DESC, mantis_bug_table.last_updated DESC, mantis_bug_table.date_submitted DESC"
var_dump($t_query_clauses['where_values']); // array(1,10,50,60,70,80)
合わせると
SELECT DISTINCT mantis_bug_table.* FROM mantis_bug_table JOIN mantis_project_table ON mantis_project_table.id = mantis_bug_table.project_id WHERE mantis_project_table.enabled = 1 AND ( mantis_bug_table.project_id = 5 ) AND ( ( mantis_bug_table.status in (10, 50, 60, 70, 80) ) ) ORDER BY mantis_bug_table.sticky DESC, mantis_bug_table.last_updated DESC, mantis_bug_table.date_submitted DESC;
フィルターやプロジェクト選択で細部は変わると思われるが、そこは問題無いはず。
JOINやWHEREが綺麗に変数分けされているので、この変数群にうまく重複数を追加するようにSQLを足せばいいはず。
重複数を計算するSQL
mantisの関係はDBのmantis_bug_relationship_table
に入っている。構造はこんな感じ。
mysql> show columns from mantis_bug_relationship_table;
+--------------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| source_bug_id | int(10) unsigned | NO | MUL | 0 | |
| destination_bug_id | int(10) unsigned | NO | MUL | 0 | |
| relationship_type | smallint(6) | NO | | 0 | |
+--------------------+------------------+------+-----+---------+----------------+
4 rows in set (0.10 sec)
幸いにも、重複元 重複先関係は、source_bug_id
とdestination_bug_id
で表されている(重複先がsource_bug_id
に現れることはない)。
よって、単純に、relationship_type
が重複元のsource_bug_id
をグループ化してやれば良い。
以下で求められる(ちょっとcount(*)
とかあやしい)。
mysql> select source_bug_id, count(*) as count from mantis_bug_relationship_table where relationship_type = 0 group by source_bug_id;
+---------------+-------+
| source_bug_id | count |
+---------------+-------+
| 169 | 2 |
| 324 | 1 |
+---------------+-------+
2 rows in set (0.00 sec)
これを、の変数に組み込む。
課題一覧の出力に重複数を追加する
SQLに疎いのでかなり悪戦苦闘しましたが、JOINで以下な感じになりました。
// カスタマイズ
$t_select_string = $t_select_string . ', COALESCE(mantis_bug_relationship_table.count, "") as duplicate_count';
$t_join_string = $t_join_string . ' LEFT JOIN (select source_bug_id, count(*) as count from mantis_bug_relationship_table where relationship_type = 0 group by source_bug_id) as mantis_bug_relationship_table ON mantis_bug_table.id = mantis_bug_relationship_table.source_bug_id';
$t_result = db_query_bound( "$t_select_string $t_from_string $t_join_string $t_where_string $t_order_string", $t_query_clauses['where_values'], $p_per_page, $t_offset );
単純にLEFT JOIN
で副問い合わせを結合し、重複が無い行にはNULL
が入るのでCOALESCE()
で空文字に初期化します。初期化は0でもよかったですが、コメント数などを参考に空に。ここでCOALESCE()
を使ったことでas
で名前付け出来たのが、もしかしたらよかったかもしれません。
再度$rows
をvar_dump
し、結果のオブジェクトにduplicate_count
が増えていればOKです。(まだHTMLには反映されません。)
課題一覧に表示させる
課題一覧に表示させるには、表示項目管理の表示項目の欄にカラム名を追加します。(管理でもアカウント毎でも)
詳しくはMantisの使い方(検索とCSV出力の表示項目) - Symfoware
課題一覧なので、「表示項目」にduplicate_count
を追加したいのですが、そのままだと「全項目」に存在しないと弾かれる。「全項目」はreadonly
で編集不可という状態です。
「全項目」にduplicate_count
を追加する。
「表示項目管理」画面のURLから/manage_config_columns_page.phpを調査し、何所から全項目を引っ張ってきているのかを調べます。
短いですね。共通部分と画面部分の読み込みのみですので、肝心の/manage_columns_inc.phpを調べます。
怪しいのは48行目の$t_columns = columns_get_all( $t_project_id );ですね。その後のHTMLの作り方から見てもこれです。
columns_get_all()
は/core/columns_api.phpの175行目です
戻り値は配列なのでreturn
前に追加しちゃいます。
$t_columns[] = 'duplicate_count'; // 無理矢理カラム追加
return $t_columns;
}
ブラウザから「全項目」の最後に, duplicate_count
が増えていればOKです。
「表示項目」にも, duplicate_count
が追加できるようになったので、追加しましょう。
duplicate_count
をソート可能にする
課題一覧を見ると画像の様に列が増えているはずです。
他のリンクになっている列名と同じく、リンクをクリックすると、ソートしようとします。
また、検索フィルターの並び順にも追加されています。
ですが、これらを使ってduplicate_count
でソートすると、SQLエラーになります。
原因は、カスタマイズ項目でもプラグイン設定されてもいない項目=デフォルト項目のソート時に、mantis_bug_table
をつけようとするからです。
つまりORDER by mantis_bug_table.duplicate_count
でソートしようとするのですが、duplicate_count
は副問い合わせ結果なので、エラーになります。
命名時にas mantis_bug_table.duplicate_count
と横着しようとしましたが、これも怒られました。当然ですね。
なので、duplicate_count
でのソート時はmantis_bug_table
を付けないようにする必要があります。
ORDER BY
はどこで作成されているか。
重複数の追加のときにすでに出ています。
var_dump($t_order_string); // " ORDER BY mantis_bug_table.sticky DESC, mantis_bug_table.last_updated DESC, mantis_bug_table.date_submitted DESC"
$t_order_string
は/core/filter_api.phpの2048行目で$t_query_clauses['order']
から組み立てられており、
$t_query_clauses['order']
で既にテーブル名は付加済み。
$t_query_clauses['order']
は/core/filter_api.phpの2033行目で、filter_get_query_sort_data()
に$t_query_clauses
を渡し、そこで['order']
を追加した上で$t_query_clauses
に上書きします。
filter_get_query_sort_data()
は同じく、/core/filter_api.phpの892行目
で、よく見かける3種類の分類をしています。
- カスタムフィールド用
- プラグインフィールド用
- その他デフォルト用
デフォルト処理が以下
# standard column
} else {
$t_sort_col = "$t_bug_table.$c_sort";
# when sorting by due_date, always display undefined dates last
if( 'due_date' == $c_sort && 'ASC' == $c_dir ) {
$t_sort_due_date = "$t_sort_col = 1";
$p_query_clauses['select'][] = $t_sort_due_date;
$t_sort_col = "$t_sort_due_date, $t_sort_col";
}
$p_query_clauses['order'][] = "$t_sort_col $c_dir";
}
$t_sort_col = "$t_bug_table.$c_sort";
でテーブル名が付加されているので、else
の前にelse if
を追加して、duplicate_count
専用の処理を追加します。
} else if($c_sort == 'duplicate_count') {
$p_query_clauses['order'][] = "$c_sort $c_dir";
# standard column
} else {
これでSQLエラーがでなくなり、ソートが可能になりました。
Excel エクスポートに表示させる
Excel エクスポートに使用するカラムは、表示項目管理の「Excel 項目」です。課題一覧と同じく、ここにも, duplicate_count
を追加し、更新します。
まずは出力
個人的にはHTMLより単純なので、これでカラムが増え、あとは和訳だけだと思っていたのですが…ファイルが出力されなくなる。
Excel エクスポート機能は/excel_xml_export.php
よくわからないので/excel_xml_export.phpにini_set( 'display_errors', 1 );
を書いて、再読み込み。今度はファイル保存のダイアログが出て、開いたxmlには
Fatal error: Call to undefined function excel_format_duplicate_count() in /mantisbt-1.2.17/excel_xml_export.php on line 84
$t_function = 'excel_format_' . $t_column;
echo $t_function( $t_row );
と、どうやらカラム毎に値の出力用関数が必要なようなので、excel_format_duplicate_count()
を作成します。
場所は/core/excel_api.phpです。
print_column_title_duplicate_count
のときの様に、他の関数をコピーして…
function excel_format_duplicate_count( $p_bug ) {
return excel_prepare_string( $p_bug->duplicate_count );
}
です。excel_prepare_string()は& < >
のHTML(XML?) エンティティ化で、整数値と空文字のduplicate_count
には不要なのですが…やっぱりお約束でつけておきます。
これでxlsの出力が可能になりました。値は正常に反映され、1行目はdulicate_count
になります。
項目名を変更する
では最後にdulicate_count
に項目名をつけ日本語にします。
あやしいのはexcel_xml_export.phpの62行目
echo excel_get_titles_row();
定義は/core/excel_api.phpの107行目
カラムごとに$t_ret .= excel_format_column_title( column_get_title( $t_column ) );
としています。
excel_api.phpのexcel_format_column_title()はカラム名をExcel用のXMLにくるんでいるだけなので、/core/columns_api.phpのcolumn_get_title()ですね。
これ、結構custom_function_default_print_column_title()と似ていて、の割にはオーバーライド不可だったりして、もやっとしてしまいました。
column_get_title()は同様にカスタム、プラグイン、その他で処理が分かれていますが、if else
を使わずガードというか関数から早く返す3が実践できてますね。
(このガードが使うべき特殊な例外であるかは別問題ですが、個人的にはelse(ネスト)
は極力減らしたいです。)
閑話休題。
で、カスタムでもプラグインでむにゃむにゃでもないので、switch文
に入ります。そこでcase 'bugnotes_count'
の様に
case 'duplicate_count':
return '重複している課題';
といれれば完成しそうなんですが…
現状入っている
default:
return lang_get_defaulted( $p_column );
で、恐らく言語ファイルから訳を取ってきているように見えます。
カスタマイズの気楽さが
言語ファイルへの追加 >> coreのphpファイルの編集
なので、case
の追加はせず、言語ファイルに追加してみます。
(lang_*系の処理は読み飛ばしたんですが、まぁやりたいことはどこでも同じだろうということで…)
対象ファイルは/lang/strings_japanese.txt
見た感じルールが$s_カラム名 = 訳
なので、$s_duplicate_count = '重複している課題';
と追記して保存。
これでExcel エクスポートも日本語になりました!
ついでにデフォルト処理に引っ張られ、課題一覧も日本語化されたので、これにて完成です。
欠番:項目名を変更する
欠番説明:このセクションは本来、「全項目」にduplicate_count
を追加する。の次にありました。
差し替え項で説明しているかもしれませんが、以下の作業を行っても、フィルターからduplicate_count
のソートが可能であり、同様のエラーが発生します。
そこで、差し替え項ではソートが可能になるようにしましたが、項目名の変更というカスタマイズの手法として、欠番扱いで残しておきます。
…の前に!この段階の課題一覧の重複数表示は恐らくこんな感じなはず。
(全部重複無しなので空文字""が入っています(悪い例画像))
で、カラム名がリンクになっています。クリックすると、ソートしようとします。ですけど、duplicate_count
は無理矢理つけたので、当然失敗し、エラーになります。そして
duplicate_count
をソート対象に選択したという記録が残り、以後検索画面を表示するたびにエラーになります。
別のカラムをソート対象に設定し直そうとしても、そもそもそのための検索画面がエラーで表示されません。
ログアウトしても無駄です。詰みです。
というぐらい焦りましたが、「表示項目」から一旦, duplicate_count
を削除すれば大丈夫です。
重複数のソートができればそれにこしたことはないですが、ちょっとキツイので、コメント数などと同様にソート不可にしたいです。
では、カラム名がどうやって出力されているか追います。
/view_all_inc.phpの163行目からがカラム名の出力ですね。
helper_call_custom_function()
を使っているということは、関数をオーバーライドできるのですが…オーバーライドの仕組みを飛ばし、実際に呼ばれるデフォルトのcustom_function_default_print_column_title()を見ます4。
中身は大きくif else
でカスタム項目とその他に分かれていて、今回はその他ですね。
} else {
$t_plugin_columns = columns_get_plugin_columns();
$t_function = 'print_column_title_' . $p_column;
if( function_exists( $t_function ) ) {
$t_function( $t_sort, $t_dir, $p_columns_target );
} else if ( isset( $t_plugin_columns[ $p_column ] ) ) {
$t_column_object = $t_plugin_columns[ $p_column ];
print_column_title_plugin( $p_column, $t_column_object, $t_sort, $t_dir, $p_columns_target );
} else {
echo '<td>';
print_view_bug_sort_link( column_get_title( $p_column ), $p_column, $t_sort, $t_dir, $p_columns_target );
print_sort_icon( $t_dir, $t_sort, $p_column );
echo '</td>';
}
}
で、その中でも3種類に別れ、
-
print_column_title_カラム名()
の関数があればそれを実行 - プラグインでむにゃむにゃしているならそれ
- それ以外はデフォルトでソートリンク付きHTML
のようです。
先の画像の例は3のデフォルトのようです。
プラグインはよくわからないし、せっかく仕組みを用意していくれているので、今回は1のprint_column_title_duplicate_count()
を作成します。
(custom_function_default_print_column_title()
はせっかくカスタマイズできる仕様なのにしませんでした。custom_function_override_print_column_title()
を作って、duplicate_count
のときはこう!というのを追加するのもいいと思います。)
といっても、適当なprint_column_title_*()
を拾ってさらっと改変
function print_column_title_duplicate_count( $p_sort, $p_dir, $p_columns_target = COLUMNS_TARGET_VIEW_PAGE ) {
echo '<td> 重複先数 </td>';
}
場所は/core/columns_api.phpです。
引数はなくてもいいと思いますが、お約束で。
文字の位置は他の雰囲気的に<td class="left">
などで出来そうカモ。
これで課題一覧での出力はひとまず完成です。
日本語でソート無しのカラムになりました。