Trac

来自Gea-Suan Lin's Wiki
跳到导航 跳到搜索

Trac是一套问题追踪系统英语:Issue tracking system)。

简介

优点

  • 简单,专注在事情的记录上,而不是限制行为上。

缺点

  • 目前的版本(1.4)还是不支援Python 3,但1.5已经在开发了。目前支援Python 3的1.6版已经出了。

安装

我是将Python装在service-trac这个使用者的pyenv里面(权限会是service-trac:service-trac)。在装完Python后,用pip安装以下套件:

sudo apt install -y libmysqlclient-dev
pip install mysqlclient pytz PyMySQL Pygments Trac

使用MariaDB时需要把改变dependency:

sudo apt install -y libmariadb-dev
pip install mysqlclient pytz PyMySQL Pygments Trac

使用MySQL或是MariaDB作为后端数据库时,建议用utf8mb4作为基础,这样数据库可以存所有范围的Unicode字元[1],另外建立独立的使用者供Trac使用:

CREATE DATABASE trac DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
CREATE USER `trac`@`127.0.0.1` IDENTIFIED BY 'password_here';
GRANT ALL ON trac.* TO `trac`@`127.0.0.1`;

接下来就可以产生环境:

cd ~; trac-admin trac initenv

这边在安装Trac时的数据库连线设定会是mysql://trac:password_here@127.0.0.1/trac(需要自己修改对应字段)。

在安装套件时,软件本体常常会放在Subversion的服务器内,在透过pip或是easy_install时会呼叫Subversion,所以会需要安装对应的软件:

sudo apt install -y subversion

另外要建立目录(放附件之类的东西):

cd ~; mkdir trac/files

FastCGI

然后是Trac的FastCGI档案,放在Trac的project目录下,并且用chmod 755 trac.fcgi改成可执行,这边的num是指跑daemon的数量,通常可以设定成与CPU或是vCPU数量相同的数字:

#!/usr/bin/env python3

import os
from trac.web.main import dispatch_request
import trac.web._fcgi

num = 4
sockaddr = os.path.expanduser('~') + '/trac/trac.sock'
os.environ['TRAC_ENV'] = os.path.dirname(__file__)

fcgiserv = trac.web._fcgi.WSGIServer(dispatch_request, bindAddress = sockaddr, umask = 7)

for _ in range(num - 1):
    if 0 == os.fork():
        break

fcgiserv.run()

另外使用userland systemd(需要先启用这个设定),让Trac在开机时将FastCGI daemon跑起来,这个档案放在~/.config/systemd/user/trac-fcgi.service

[Unit]
Description=Trac FastCGI daemon
After=remote-fs.target

[Service]
ExecStart=/bin/bash -l -c "exec ~/trac/trac.fcgi"
Type=simple

[Install]
WantedBy=default.target

然后让systemd重新读取,并且设定为开机启动,最后手动先跑起来(或是重开机):

systemctl --user daemon-reload
systemctl --user enable --now trac-fcgi.service

另外要让nginx可以存取/var/run/trac/trac.sock,然后完全重跑nginx:

sudo useradd -G service-trac www-data; sudo service nginx restart

套件

目前有安装的套件

除了基本安装外,还会安装这些套件:

目前Trac 1.4版的安装方式是:

#!/bin/bash
pip install -U TracCronPlugin
pip install -U genshi
pip install -U git+https://github.com/gslin/trac-subtickets-plugin.git
pip install -U git+https://github.com/gslin/trac-slack-plugin.git
pip install -U requests
pip install -U svn+https://trac-hacks.org/svn/accountmanagerplugin/trunk/
pip install -U svn+https://trac-hacks.org/svn/duplicateticketsearchplugin/trunk/
pip install -U svn+https://trac-hacks.org/svn/graphvizplugin/branches/1.2/
pip install -U svn+https://trac-hacks.org/svn/tracdragdropplugin/0.12/
pip install -U svn+https://trac-hacks.org/svn/tracwysiwygplugin/0.12/
pip install -U svn+https://trac-hacks.org/svn/xmlrpcplugin/trunk/

对应的requirements.txt是:

-e git+https://github.com/gslin/trac-subtickets-plugin.git
-e git+https://github.com/gslin/trac-slack-plugin.git
-e svn+https://trac-hacks.org/svn/accountmanagerplugin/trunk/
-e svn+https://trac-hacks.org/svn/duplicateticketsearchplugin/trunk/
-e svn+https://trac-hacks.org/svn/graphvizplugin/branches/1.2/
-e svn+https://trac-hacks.org/svn/tracdragdropplugin/0.12/
-e svn+https://trac-hacks.org/svn/tracwysiwygplugin/0.12/
-e svn+https://trac-hacks.org/svn/xmlrpcplugin/trunk/
TracCronPlugin
genshi
requests

可以用pip install -r requirements.txt安装。

曾经装过的套件

以前会安装,但现在因为自己用而没有装上(没有需求或是不想装):

设定

权限

透过CLI的方式可以更新权限:

trac-admin /path/to/myproject permission add admin TRAC_ADMIN

Trac预设的权限设定过于严格,只给内部使用的情境下,建议对authenticated群组加上:

  • TICKET_EDIT_CC
  • TICKET_EDIT_DESCRIPTION

conf/trac.ini

记得修改Logo网址:

[header_logo]
alt = Logo
link = https://trac.example.com/
src = https://static.example.com/logo.png

Trac可以设定预设发通知信件:

[notification]
smtp_default_domain = example.com
smtp_enabled = enabled
smtp_from = trac@trac.example.com
smtp_replyto = noreply@trac.example.com
use_public_cc = enabled

我自己的Report不想分页(预设是一页100笔),所以设为:

[report]
items_per_page = 0

另外因为我们不太用Trac内建的Wiki,但又关不掉,所以只能针对没找到的页面就不要产生连结了:

[wiki]
ignore_missing_pages = enabled

htdoc/style.css

  • 修正button因为CSS效果而有时会按不到的问题。
  • 针对code block内一行过长时自动换行。
  • 针对今天到期与过期的票用不同的标示标出(配合下方的JavaScript)。
  • 将已经关掉的票变淡(配合下方的JavaScript)。
  • 让编辑区域使用等宽字型。
  • 贴超长字串的时候不要产生横向卷轴。
  • 让可用范围变宽。
  • 让图片不要超过萤幕宽度。
#changelog, #ticket {
    overflow-wrap: break-word;
}
#content.ticket {
    width: 78em;
}
#main img {
    max-width: 100%;
}
input[type=button]:active, input[type=submit]:active,
input[type=reset]:active {
    position: relative;
    top: 0;
    left: 0;
}
pre {
    white-space: pre-wrap;
}
table.listing.tickets td {
    vertical-align: middle;
}
table.listing.tickets td.due_date, table.listing.tickets td.reporter {
    white-space: nowrap;
}
table.tickets tr.duedate_overdue {
    font-weight: bold;
}
table.tickets tr.duedate_today {
    border: 2px solid;
}
table.subtickets tr td:last-of-type {
    width: 15em;
}
table.subtickets tr.ticket_closed, .closed.ticket {
    opacity: 0.5;
}
textarea {
    font-family: monospace;
}

template/site_head.html

这是Trac 1.4的Jinja2设计,如果是Trac 1.2或更早版本的使用者,可以在template/site.html里面增加。

<meta name="referrer" content="no-referrer" />
<link rel="stylesheet" href="${href.chrome('site/style.css')}" />
<script src="https://cdn.jsdelivr.net/npm/strftime@0.10.0/strftime.min.js"></script>

template/site_footer.html

这是Trac 1.4的Jinja2设计,如果是Trac 1.2或更早版本的使用者,可以在template/site.html里面增加。

  • 用JavaScript针对今天到期以及过期的票增加CSS。
  • 将新票里的Due Date改为零点零分零秒。
  • 增加把票直接延到下个月月底的button。
  • 增加button,让关票可以直接点击。
  • 将票里的attachments与modify内容展开。
  • 将开票人的资讯放到Action字段里,比较好确认这张票的情况。
  • 将日历选择器中,关闭动画效果[2],另外将每周的第一天设为星期天[2],并且允许选择其他月份的日期[2]
  • 对所有button都设定accesskey。
<script>
<!--
'use strict';

// Run immediately.
(function($) {
    // Due date css handling
    let d = new Date();
    let today = (new Date(d.getTime() - d.getTimezoneOffset() * 60000)).toISOString().slice(0, 10);
    document.querySelectorAll('table.tickets td.due_date').forEach(function(el) {
        let due = el.innerText.trim();
        if (due < today) {
            el.parentElement.classList.add('duedate_overdue');
        } else if (due === today) {
            el.parentElement.classList.add('duedate_today');
        }
    });

    // Due date (newticket) set to 00:00:00 (in UTC)
    if ($('form[action$="/newticket#ticket"]').length > 0) {
        do {
            if ($('#ticket:visible').hasClass('ticketdraft')) {
                break;
            }

            let el = $('#field-due_date');
            let t = el.val();

            if ('Z' === t.slice(-1)) {
                t = new Date(Date.parse(t) + 86400000);
                t = strftime('%Y-%m-%dT00:00:00Z', t);
                el.val(t);
                break;
            }

            let a = t.match(/\+(\d\d:\d\d)$/);
            if (null !== a) {
                let tz = a[1];
                let new_str = 'T' + tz + ':00+' + tz;
                t = new Date(Date.parse(t) + 86400000);
                t = strftime('%Y-%m-%d' + new_str, t);
                el.val(t);
                break;
            }
        } while (false);
    }

    // Due date postpone button
    let el = $('<button>Postpone (month)</button>');
    el.click(function(){
        let el = $('#field-due_date');
        let t = el.val();

        do {
            if ('Z' === t.slice(-1)) {
                t = new Date(Date.now());
                t.setDate(1);
                t.setMonth(t.getMonth() + 2);
                t.setDate(0);
                t = strftime('%Y-%m-%dT00:00:00Z', t);
                el.val(t);
                break;
            }

            let a = t.match(/\+(\d\d:\d\d)$/);
            if (null !== a) {
                let tz = a[1];
                let new_str = 'T' + tz + ':00+' + tz;
                t = new Date(Date.now());
                t.setDate(1);
                t.setMonth(t.getMonth() + 2);
                t.setDate(0);
                t = strftime('%Y-%m-%d' + new_str, t);
                el.val(t);
                break;
            }
        } while (false);

        $('#propertyform input[name="submit"]').click();
    });
    $('td[headers="h_due_date"]').append(el);

    // Accept & start buttons
    $('#action_accept, #action_start').each(function(){
        var v = $(this).val();

        var that = this;
        var b = $('<button/>').click(function(e){
            var v = $(this).val();
            $(that).click();
            $('#propertyform input[name="submit"]').click();
            e.preventDefault();
        });
        b.prop('value', v).text(v);

        $(this).closest('div').append(b);
    });

    // Resolve with Buttons
    $('#action_resolve_resolve_resolution option').each(function(){
        var v = $(this).val();

        var b = $('<button/>').click(function(e){
            var v = $(this).val();
            $('#action_resolve').click();
            $('#action_resolve_resolve_resolution').prop('disabled', false);
            $('#action_resolve_resolve_resolution').find('option[value="' + v + '"]').prop('selected', 'selected');
            $('#propertyform input[name="submit"]').click();
            e.preventDefault();
        });
        b.prop('value', v).text(v);

        $(this).closest('div').append(b);
    });

    // Run after content loaded.
    $(function() {
        // Reset Drag & Drop hovering issue.
        let el = document.querySelector('.tracdragdrop-paste.beautytips');
        el?.parentElement.addEventListener('mouseover', ev => {ev.stopPropagation();}, true);

        // Layout
        $('#attachments').removeClass('collapsed');
        $('#modify').parent().removeClass('collapsed');

        // Datepicker
        if ($.datepicker) {
            $.datepicker.setDefaults({
                firstDay: 0,
                selectOtherMonths: true,
                showAnim: '',
                showOtherMonths: true
            });
        }

        // Add accesskey
        $('button').each(function() {
            var el = $(this);

            var a = el.text()[0].toLowerCase();
            el.attr('accesskey', a);
        });

        // Add submit accesskey
        $('input[name="submit"]').each(function() {
            $(this).attr('accesskey', 'c');
        });

        // Closed tickets handling
        $('tr:has(a.closed)').addClass('ticket_closed');
    });
})(jQuery);

//-->
</script>

其他

删除使用者时可能会需要手动到以下表格清除资料(清完后需要重跑Trac,因为Trac会读出来cache一份),避免autocomplete之类的套件仍然显示:

  • auth_cookie
  • session
  • session_attribute

著名使用单位

  • Django
  • FFmpeg
  • jQuery
  • MacPorts
  • nginx
  • OpenVPN
  • Tor(2020年六月终止[3]
  • VirtualBox
  • WebKit
  • WordPress

相关连结

参考资料

  1. MySqlDb – The Trac Project. [2018-03-01]. 
  2. 2.0 2.1 2.2 Datepicker Widget | jQuery UI API Documentation. [2018-02-28]. 
  3. From Trac into Gitlab for Tor. 2020-11-20 [2020-11-22] (English). 

外部链接