Trac Aggregtor
Trac 是我目前正在用的 issue tracking system,由於整合了 wiki 與 subversion,故幾乎可以拿來當作 project management system 來用了。不過,目前的 Trac 即使是進展到了 1.0 版,也仍然將是 single project 的系統。然而,實際工作上,多個 project 一併進行,設置互有牽扯的情況非常普遍。因此,我寫了一個簡單的 PHP 網頁,剛好可以簡單地當作 multi-project trac portal 來用。
使用前,請先依照 TracMultiProjects 的說明,設定好 multi-project 的環境。然後,修改這個 PHP 網頁最前面的 define(),TRAC_DIR 是指你放那堆 trac environment 的目錄,而 TRAC_USR 則是你登入 trac 用的帳號。最後,把這個網頁放在你覺得最適合的地方即可。
<?php
define('TRAC_DIR', '/trac');
define('TRAC_USR', 'jeffhung');
$tracagg_errors = array();
function tracagg_raise_error($errmsg)
{
array_push($tracagg_errors, $errmsg);
}
function wrap_html_text($text)
{
return (($text == '') ? ' ' : htmlspecialchars($text));
}
function ticket_priority_cmp($lhs, $rhs)
{
static $TRAC_PRIORITY_LIST = array(
'lowest',
'low',
'normal',
'high',
'highest'
);
$lhs_code = array_search($lhs['priority'], $TRAC_PRIORITY_LIST);
$rhs_code = array_search($rhs['priority'], $TRAC_PRIORITY_LIST);
if ($lhs_code == $rhs_code) {
return 0;
}
return ($lhs_code > $rhs_code) ? -1 : 1;
}
function ticket_severity_cmp($lhs, $rhs)
{
static $TRAC_SEVERITY_LIST = array(
'enhancement',
'trivial',
'minor',
'normal',
'major',
'critical',
'blocker'
);
$lhs_code = array_search($lhs['severity'], $TRAC_SEVERITY_LIST);
$rhs_code = array_search($rhs['severity'], $TRAC_SEVERITY_LIST);
if ($lhs_code == $rhs_code) {
return 0;
}
return ($lhs_code > $rhs_code) ? -1 : 1;
}
function ticket_project_cmp($lhs, $rhs)
{
$lhs_code = $lhs['__project__'];
$rhs_code = $rhs['__project__'];
return strcasecmp($lhs_code, $rhs_code);
}
function ticket_milestone_cmp($lhs, $rhs)
{
$lhs_code = $lhs['milestone'];
$rhs_code = $rhs['milestone'];
return strcasecmp($lhs_code, $rhs_code);
}
function ticket_changetime_cmp($lhs, $rhs)
{
$lhs_code = $lhs['_changetime'];
$rhs_code = $rhs['_changetime'];
return ($lhs_code < $rhs_code);
}
function composite_cmp_(&$block, $func_specs)
{
if (count($func_specs)) {
$block .= '$r = ';
$block .= $func_specs[0]['func'];
if ($func_specs[0]['rev']) {
$block .= '($rhs, $lhs);';
}
else {
$block .= '($lhs, $rhs);';
}
$block .= 'if ($r == 0) {';
array_shift($func_specs);
composite_cmp_($block, $func_specs);
$block .= '}';
}
}
function create_composite_cmp($func_specs)
{
$func_block = '';
composite_cmp_($func_block, $func_specs);
$func_block .= 'return $r;';
return create_function('$lhs, $rhs', $func_block);
}
function GetTracTickets($trac_dir, $project, $user)
{
$tickets = false;
$env = sprintf('%s/%s', $trac_dir, $project);
$db = sqlite_open(
sprintf('%s/db/trac.db', $env),
0666,
$db_errmsg
);
if ($db === false) {
tracagg_raise_error($db_errmsg);
}
else {
$sql = sprintf(
"SELECT ".
" p.value AS __color__, ".
" ( ".
" CASE owner WHEN '$user' THEN ".
" ( milestone || ' (mine)' ) ".
" ELSE ".
" ( milestone || ' (others)' ) ".
" END ".
" ) AS __group__, ".
" ( ".
" CASE status WHEN 'closed' THEN ".
" 'color: #777; background: #ddd; border-color: #ccc;' ".
" ELSE ".
" ( ".
" CASE owner WHEN '$user' THEN ".
" 'font-weight: normal' ".
" END ".
" ) ".
" END ".
" ) AS __style__, ".
" t.id AS ticket, ".
" t.summary AS summary, ".
" t.component AS component, ".
" t.version AS version, ".
" t.milestone AS milestone, ".
" t.severity AS severity, ".
" t.priority AS priority, ".
" ( ".
" CASE t.status WHEN 'assigned' THEN t.owner || ' *' ".
" ELSE t.owner ".
" END ".
" ) AS owner, ".
" t.time AS created, ".
" t.changetime AS _changetime, ".
" t.description AS _description, ".
" t.reporter AS _reporter ".
"FROM ".
" ticket t, ".
" enum p ".
"WHERE ".
" p.name = t.priority AND ".
" p.type = 'priority' AND ".
" status IN ('new', 'assigned', 'reopened') AND ".
" 1 "
// "ORDER BY ".
// " (t.milestone = ''), ".
// " t.milestone, ".
// " (t.status = 'closed'), ".
// " (t.owner = '$user') DESC, ".
// " p.value, ".
// " t.severity, ".
// " t.time "
);
$res = sqlite_query($db, $sql);
if ($res === false) {
tracagg_raise_error("Failed to do SQL query: $sql");
}
else {
$tickets = array();
while ($row = sqlite_fetch_array($res, SQLITE_ASSOC)) {
$row['__project__'] = $project;
array_push($tickets, $row);
}
}
sqlite_close($db);
}
return $tickets;
}
$TotalProjectList = array();
if (is_dir(TRAC_DIR)) {
if ($dh = opendir(TRAC_DIR)) {
while (($entry = readdir($dh)) != false) {
if (substr($entry, 0, 1) != '.') {
if (is_dir(TRAC_DIR . "/$entry")) {
array_push($TotalProjectList, $entry);
}
}
}
closedir($dh);
}
}
sort($TotalProjectList);
$TicketAggregation = array();
foreach ($TotalProjectList as $prj) {
if (in_array($prj, array_keys($_REQUEST))) {
$tickets = GetTracTickets(TRAC_DIR, $prj, TRAC_USR);
$TicketAggregation = array_merge($TicketAggregation, $tickets);
}
}
usort($TicketAggregation, create_composite_cmp(array(
array('func' => 'ticket_changetime_cmp', 'rev' => false ),
array('func' => 'ticket_priority_cmp', 'rev' => false ),
array('func' => 'ticket_severity_cmp', 'rev' => false ),
array('func' => 'ticket_project_cmp', 'rev' => false ),
array('func' => 'ticket_milestone_cmp', 'rev' => false ),
)));
?><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>TracAggregator</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="Author" content="">
<meta name="Keywords" content="">
<meta name="Description" content="">
<link rel="stylesheet" href="trac-agg.css" />
</head>
<body>
<?php //print_r($_REQUEST); ?>
<div id="project_list">
<form name="PrjListForm" action="<?php
echo htmlspecialchars($_SERVER['PHP_SELF']);
?>" method="GET">
<fieldset>
<legend>Select Projects</legend>
<?php
foreach ($TotalProjectList as $prj) {
printf("<span>");
printf("<input type=\"checkbox\" id=\"%s\" name=\"%s\"".
" onChange=\"document.PrjListForm.submit();\" %s/>",
htmlspecialchars($prj),
htmlspecialchars($prj),
(in_array($prj, array_keys($_REQUEST)) ? 'checked' : '')
);
printf(" <a href=\"/projects/%s\" target=\"_blank\">%s</a>",
htmlspecialchars($prj),
htmlspecialchars($prj)
);
printf(" [<a href=\"/projects/%s/newticket\" target=\"_blank\">+</a>]",
htmlspecialchars($prj)
);
printf("</span>\n");
}
?>
</fieldset>
</form>
</div>
Total <?php echo count($TicketAggregation); ?> ticket(s):
<table id="ticket_list" width="100%">
<thead>
<tr>
<th>project</th>
<th>ticket</th>
<th>summary</th>
<th>component</th>
<th>version</th>
<th>milestone</th>
<th>severity</th>
<th>priority</th>
<th>owner</th>
<th>created</th>
<th>changed</th>
</tr>
</thead>
<tbody>
<?php
foreach ($TicketAggregation as $ta) {
printf("<tr style=\"%s\">\n", htmlspecialchars($ta['__style__']));
printf("<td>%s</td>\n", wrap_html_text($ta['__project__']));
printf("<td>%s</td>\n", wrap_html_text('#' . $ta['ticket']));
printf(
"<td><a href=\"/projects/%s/ticket/%d\" target=\"_blank\">%s</a></td>\n",
$ta['__project__'],
$ta['ticket'],
wrap_html_text($ta['summary'])
);
printf("<td>%s</td>\n", wrap_html_text($ta['component']));
printf("<td>%s</td>\n", wrap_html_text($ta['version']));
printf("<td>%s</td>\n", wrap_html_text($ta['milestone']));
printf("<td>%s</td>\n", wrap_html_text($ta['severity']));
printf("<td>%s</td>\n", wrap_html_text($ta['priority']));
printf("<td>%s</td>\n", wrap_html_text($ta['owner']));
printf("<td>%s</td>\n", wrap_html_text(strftime('%Y-%m-%d', $ta['created'])));
printf("<td>%s</td>\n", wrap_html_text(strftime('%Y-%m-%d %H:%M:%S', $ta['_changetime'])));
printf("</tr>\n");
}
?>
</tbody>
</table>
</body>
</html>
程式還會再繼續修改,本文將隨時更新。
Screenshot:
Random Posts
- None Found
Similar Posts
- None Found

2 Comments
我同事也曾在公司內試架過 Trac,不過由於已事先用了 XPlanner + JSPWiki 當專案管理站台,就不想冒然換台。看到你將 Trac 用的這樣出神入化,真是令人敬佩!
你好,請問存取 sqlite3 的話,code 要怎樣改呢? 我改用 PDO 去存取,但失敗了...
2 Backlinks
The design of trac aggregator
前陣子我在 blog 上簡單介紹了 Trac Aggregator,因為在工作上總是必須多個專案同時進行,相互依存,又因為當初規劃 repository 的決策,造成搭配 Trac 時,因為 Trac 沒有 multi-project support,而使
回應 Edward: Trac Aggregator
我看了 XPlanner 的 screenshots,
感覺這也是相當強大的軟體。我現在比較會碰到的困擾是,issue tracker 常會無法與 process 整合。要把 issue
tracker 用的好,勢必要有一套 process 配合。但因為人工
Post a Comment