「Trac」:修訂間差異

出自Gea-Suan Lin's Wiki
跳至導覽 跳至搜尋
Gslin留言 | 貢獻
Gslin留言 | 貢獻
 
(未顯示同一使用者於中間所作的 72 次修訂)
第2行: 第2行:


== 簡介 ==
== 簡介 ==
=== 優點 ===
=== 優點 ===
* 簡單,專注在事情的記錄上,而不是限制行為上。
* 簡單,專注在事情的記錄上,而不是限制行為上。


=== 缺點 ===
=== 缺點 ===
* 目前還是不支援Python 3。
 
* <del>目前的版本(1.4)還是不支援Python 3,但1.5已經在開發了。</del>目前支援[[Python]] 3的1.6版已經出了


== 安裝 ==
== 安裝 ==
我是將[[Python]]裝在<code>www-data</code>這個使用者的[[pyenv]]裡面(權限會是<code>www-data:www-data</code>)在裝完Python後,用<code>pip</code>安裝以下套件:
 
我是將[[Python]]裝在<code>service-trac</code>這個使用者的[[pyenv]]裡面(權限會是<code>service-trac:service-trac</code>)
 
<syntaxhighlight lang="bash">
sudo useradd service-trac -m -r -s /bin/bash
</syntaxhighlight>
 
在裝完Python後,用<code>pip</code>安裝以下套件:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
sudo apt install -y libmysqlclient-dev
sudo apt install -y libmysqlclient-dev
pip install mysql-python PyMySQL Pygments Trac
pip install mysqlclient pytz PyMySQL Pygments Trac
</syntaxhighlight>
 
使用[[MariaDB]]時需要把改變dependency:
 
<syntaxhighlight lang="bash">
sudo apt install -y libmariadb-dev
pip install mysqlclient pytz PyMySQL Pygments Trac
</syntaxhighlight>
</syntaxhighlight>


使用[[MySQL]]作為後端資料庫時,建議用utf8mb4作為基礎,這樣資料庫可以存所有範圍的Unicode字元<ref>{{Cite web|url=https://trac.edgewall.org/wiki/MySqlDb|title=MySqlDb    – The Trac Project|accessdate=2018-03-01}}</ref>,另外建立獨立的使用者供Trac使用:
使用[[MySQL]]或是[[MariaDB]]作為後端資料庫時,建議用utf8mb4作為基礎,這樣資料庫可以存所有範圍的Unicode字元<ref>{{Cite web|url=https://trac.edgewall.org/wiki/MySqlDb|title=MySqlDb    – The Trac Project|accessdate=2018-03-01}}</ref>,另外建立獨立的使用者供Trac使用:


<syntaxhighlight lang="sql">
<syntaxhighlight lang="sql">
CREATE DATABASE trac DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
CREATE DATABASE trac DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
GRANT ALL ON trac.* TO `trac`@`localhost` IDENTIFIED BY 'password_here';
CREATE USER `trac`@`127.0.0.1` IDENTIFIED BY 'password_here';
GRANT ALL ON trac.* TO `trac`@`127.0.0.1`;
</syntaxhighlight>
</syntaxhighlight>


第26行: 第44行:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
cd /srv/issue.example.com
cd ~; trac-admin trac initenv
trac-admin trac initenv
</syntaxhighlight>
</syntaxhighlight>


這邊在安裝Trac時的資料庫連線設定會是<code>mysql://trac:password_here@localhost/trac</code>(需要自己修改對應欄位)。
這邊在安裝Trac時的資料庫連線設定會是<code>mysql://trac:password_here@127.0.0.1/trac</code>(需要自己修改對應欄位)。


在安裝套件時,軟體本體常常會放在[[Subversion]]的伺服器內,在透過<code>pip</code>或是<code>easy_install</code>時會呼叫Subversion,所以會需要安裝對應的軟體:
在安裝套件時,軟體本體常常會放在[[Subversion]]的伺服器內,在透過<code>pip</code>或是<code>easy_install</code>時會呼叫Subversion,所以會需要安裝對應的軟體:
第38行: 第55行:
</syntaxhighlight>
</syntaxhighlight>


另外要建立目錄,並且讓Trac可以寫入
另外要建立目錄(放附件之類的東西)


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
sudo mkdir trac/files
cd ~; mkdir trac/files
sudo chown www-data:www-data trac/files
</syntaxhighlight>
</syntaxhighlight>


然後是Trac的[[FastCGI]]檔案,放在Trac的project目錄下,並且用<code>chmod 755 trac.fcgi</code>改成可執行:
=== FastCGI ===
 
然後是Trac的[[FastCGI]]檔案,放在Trac的project目錄下,並且用<code>chmod 755 trac.fcgi</code>改成可執行,這邊的<code>num</code>是指跑daemon的數量,通常可以設定成與CPU或是vCPU數量相同的數字


<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
#!/usr/bin/env python
#!/usr/bin/env python3


import os
import os
第54行: 第72行:
import trac.web._fcgi
import trac.web._fcgi


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


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


num -= 1
for _ in range(num - 1):
while num > 0:
    num -= 1
     if 0 == os.fork():
     if 0 == os.fork():
         break
         break
第69行: 第85行:
</syntaxhighlight>
</syntaxhighlight>


另外[[Systemd]]設定讓Trac在開機時以FastCGI模式跑起來,這個檔案放在<code>/lib/systemd/system/trac-fcgi.service</code>(要記得把裡面的<code>/srv/trac.example.com/trac/</code>改成自己的目錄</code>:
另外使用userland [[systemd]](需要先啟用這個設定),讓Trac在開機時將FastCGI daemon跑起來,這個檔案放在<code>~/.config/systemd/user/trac-fcgi.service</code>:


<syntaxhighlight lang="ini">
<syntaxhighlight lang="ini">
[Unit]
[Unit]
Description=trac fcgi daemon
Description=Trac FastCGI daemon
After=remote-fs.target


[Service]
[Service]
ExecStart=/bin/bash -l -c "exec /srv/issue.example.com/trac/trac.fcgi"
ExecStart=/bin/bash -l -c "exec ~/trac/trac.fcgi"
Group=www-data
RuntimeDirectory=trac
Type=simple
Type=simple
User=www-data


[Install]
[Install]
WantedBy=multi-user.target
WantedBy=default.target
</syntaxhighlight>
</syntaxhighlight>


第89行: 第103行:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
sudo systemctl daemon-reload
systemctl --user daemon-reload
sudo systemctl enable trac-fcgi
systemctl --user enable --now trac-fcgi.service
sudo service trac-fcgi restart
</syntaxhighlight>
 
另外要讓[[nginx]]可以存取<code>/var/run/trac/trac.sock</code>,然後完全重跑nginx:
 
<syntaxhighlight lang="bash">
sudo useradd -G service-trac www-data; sudo service nginx restart
</syntaxhighlight>
</syntaxhighlight>


第99行: 第118行:


除了基本安裝外,還會安裝這些套件:
除了基本安裝外,還會安裝這些套件:
* [https://trac-hacks.org/wiki/AccountManagerPlugin AccountManagerPlugin – Trac Hacks - Plugins Macros etc.]
* [https://trac-hacks.org/wiki/AccountManagerPlugin AccountManagerPlugin – Trac Hacks - Plugins Macros etc.]
** 可以吃HTML Form Login,而不侷限在HTTP authentication。
** 可以吃HTML Form Login,而不侷限在HTTP authentication。
第107行: 第127行:
* [https://trac-hacks.org/wiki/GraphvizPlugin GraphvizPlugin – Trac Hacks - Plugins Macros etc.]
* [https://trac-hacks.org/wiki/GraphvizPlugin GraphvizPlugin – Trac Hacks - Plugins Macros etc.]
** Graphviz可以拿來畫各類圖,我自己最常用的是Dot圖。
** Graphviz可以拿來畫各類圖,我自己最常用的是Dot圖。
* [https://trac-hacks.org/wiki/HttpAuthPlugin HttpAuthPlugin – Trac Hacks - Plugins Macros etc.]
* [https://trac-hacks.org/wiki/HttpAuthPlugin HttpAuthPlugin]
** 配合AccountManagerPlugin與XmlRpcPlugin使用的,當AccountManagerPlugin啟用HTML Form Login,XMLRPC會因為不支援這種登入方式而失效而這個套件可以解這個問題
** 將HTTP header中的Authentication轉成AccountManagerPlugin也可以讀到欄位主要是使用AccountManagerPlugin與XmlRpcPlugin的戶接,PyPI上面的版本太舊已經相容現在的Trac,請安裝trunk版本。
* [https://trac-hacks.org/wiki/RobotsTxtPlugin RobotsTxtPlugin]
** 支援<code>/robots.txt</code>透過Trac的Wiki實作
* [https://trac-hacks.org/wiki/SlackIntegration SlackIntegration – Trac Hacks - Plugins Macros etc.]
* [https://trac-hacks.org/wiki/SlackIntegration SlackIntegration – Trac Hacks - Plugins Macros etc.]
** 可以把變更丟到Slack上,這邊要注意的是套件本身沒有把相依性做好,需要自己安裝<code>requests</code>套件。
** 可以把變更丟到Slack上,這邊要注意的是套件本身沒有把相依性做好,需要自己安裝<code>requests</code>套件。在Trac 1.6上面因為Python 3的原因會有問題,可以裝[https://github.com/gslin/trac-slack-plugin 我修的版本]
* [https://trac-hacks.org/wiki/SubticketsPlugin SubticketsPlugin – Trac Hacks - Plugins Macros etc.]
* [https://trac-hacks.org/wiki/SubticketsPlugin SubticketsPlugin – Trac Hacks - Plugins Macros etc.]
** 子母票的延伸套件,基本上是必備項目。
** 子母票的延伸套件,基本上是必備項目。原版的程式安裝在MySQL 8.0的環境下會出問題(也不支援Trac 1.6),可以用[https://github.com/gslin/trac-subtickets-plugin 我修的版本]
* [https://trac-hacks.org/wiki/ThemeEnginePlugin ThemeEnginePlugin – Trac Hacks - Plugins Macros etc.]
* [https://trac-hacks.org/wiki/ThemeEnginePlugin ThemeEnginePlugin – Trac Hacks - Plugins Macros etc.]
** Theme的套件管理工具,配合BlueFlatTheme用的。
** Theme的套件管理工具,配合BlueFlatTheme用的。
第124行: 第146行:
** 讓使用者可以透過API操作Trac。
** 讓使用者可以透過API操作Trac。


目前Trac 1.2版的安裝方式是:
目前Trac 1.4版的安裝方式是:


<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
#!/bin/bash
#!/bin/bash
pip install TracAccountManager
pip install -U TracCronPlugin
pip install svn+https://trac-hacks.org/svn/duplicateticketsearchplugin/trunk/
pip install -U TracRobotsTxt
pip install svn+https://trac-hacks.org/svn/graphvizplugin/branches/1.2/
pip install -U genshi
pip install svn+https://trac-hacks.org/svn/httpauthplugin/trunk/
pip install -U git+https://github.com/gslin/trac-subtickets-plugin.git
pip install git+https://github.com/wagnerpinheiro/trac-slack-plugin.git
pip install -U git+https://github.com/gslin/trac-slack-plugin.git
pip install requests
pip install -U requests
pip install git+https://github.com/trac-hacks/trac-subtickets-plugin.git
pip install -U svn+https://trac-hacks.org/svn/accountmanagerplugin/trunk/
pip install TracCronPlugin
pip install -U svn+https://trac-hacks.org/svn/duplicateticketsearchplugin/trunk/
pip install svn+https://trac-hacks.org/svn/tracdragdropplugin/0.12/
pip install -U svn+https://trac-hacks.org/svn/graphvizplugin/branches/1.2/
pip install svn+https://trac-hacks.org/svn/tracwysiwygplugin/0.12/
pip install -U svn+https://trac-hacks.org/svn/httpauthplugin/trunk/
pip install TracXMLRPC
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/
</syntaxhighlight>
</syntaxhighlight>


第144行: 第168行:


<syntaxhighlight lang="text">
<syntaxhighlight lang="text">
-e git+https://github.com/trac-hacks/trac-subtickets-plugin.git
-e git+https://github.com/gslin/trac-subtickets-plugin.git
-e git+https://github.com/wagnerpinheiro/trac-slack-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/duplicateticketsearchplugin/trunk/
-e svn+https://trac-hacks.org/svn/graphvizplugin/branches/1.2/
-e svn+https://trac-hacks.org/svn/graphvizplugin/branches/1.2/
第151行: 第176行:
-e svn+https://trac-hacks.org/svn/tracdragdropplugin/0.12/
-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/tracwysiwygplugin/0.12/
TracAccountManager
-e svn+https://trac-hacks.org/svn/xmlrpcplugin/trunk/
TracCronPlugin
TracCronPlugin
TracXMLRPC
TracHTTPAuth
TracRobotsTxt
genshi
requests
requests
</syntaxhighlight>
</syntaxhighlight>
第175行: 第202行:
** 自動把參與者加到Cc列表內,這樣才會收到後續的更新。
** 自動把參與者加到Cc列表內,這樣才會收到後續的更新。
* [https://github.com/gslin/trac-references-mail-decorator Add header's Message-ID field to References field for Trac.]
* [https://github.com/gslin/trac-references-mail-decorator Add header's Message-ID field to References field for Trac.]
** 當使用Amazon SES發信時,會因為<code>Message-ID</code>被換掉,而導致Mail Client無法透過<code>References</code>將同一張票的通知信整在一起。這個套件透過把信件本身的<code><essage-ID</code>複製一份到<code>References</code>的workaround讓Mail Client可以正確判斷出這些信件是同一張票。
** 當使用Amazon SES發信時,會因為<code>Message-ID</code>被換掉,而導致Mail Client無法透過<code>References</code>將同一張票的通知信整在一起。這個套件透過把信件本身的<code>Message-ID</code>複製一份到<code>References</code>的workaround讓Mail Client可以正確判斷出這些信件是同一張票。
* [https://github.com/gslin/trac-secret-checkbox-ticket gslin/trac-secret-checkbox-ticket: Add ticket security policy for Trac.]
* [https://github.com/gslin/trac-secret-checkbox-ticket gslin/trac-secret-checkbox-ticket: Add ticket security policy for Trac.]
** 將票設定為祕密,只有Reporter(開票人)、Owner(目前有票的人)、Cc列表內的人可以讀。
** 將票設定為祕密,只有Reporter(開票人)、Owner(目前有票的人)、Cc列表內的人可以讀。
第185行: 第212行:


=== 權限 ===
=== 權限 ===
透過CLI的方式可以更新權限:
<syntaxhighlight lang="bash">
trac-admin /path/to/myproject permission add admin TRAC_ADMIN
</syntaxhighlight>


Trac預設的權限設定過於嚴格,只給內部使用的情境下,建議對<code>authenticated</code>群組加上:
Trac預設的權限設定過於嚴格,只給內部使用的情境下,建議對<code>authenticated</code>群組加上:
第192行: 第225行:


=== conf/trac.ini ===
=== conf/trac.ini ===
記得修改Logo網址:
<syntaxhighlight lang="ini">
[header_logo]
alt = Logo
link = https://trac.example.com/
src = https://static.example.com/logo.png
</syntaxhighlight>
Trac可以設定預設發通知信件:
<syntaxhighlight lang="ini">
[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
</syntaxhighlight>


我自己的Report不想分頁(預設是一頁100筆),所以設為:
我自己的Report不想分頁(預設是一頁100筆),所以設為:
第205行: 第258行:
[wiki]
[wiki]
ignore_missing_pages = enabled
ignore_missing_pages = enabled
</syntaxhighlight>
=== htdoc/style.css ===
* 修正button因為CSS效果而有時會按不到的問題。
* 針對code block內一行過長時自動換行。
* 針對今天到期與過期的票用不同的標示標出(配合下方的JavaScript)。
* 將已經關掉的票變淡(配合下方的JavaScript)。
* 讓編輯區域使用等寬字型。
* 貼超長字串的時候不要產生橫向捲軸。
* 讓可用範圍變寬。
* 讓圖片不要超過螢幕寬度。
<syntaxhighlight lang="css">
#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;
}
</syntaxhighlight>
</syntaxhighlight>


=== template/site_head.html ===
=== template/site_head.html ===
這是Trac 1.4的Jinja2設計,如果是Trac 1.2或更早版本的使用者,可以在<code>template/site.html</code>裡面增加。


<syntaxhighlight lang="html">
<syntaxhighlight lang="html">
第216行: 第324行:


=== template/site_footer.html ===
=== template/site_footer.html ===
這是Trac 1.4的Jinja2設計,如果是Trac 1.2或更早版本的使用者,可以在<code>template/site.html</code>裡面增加。


* 用JavaScript針對今天到期以及過期的票增加CSS。
* 用JavaScript針對今天到期以及過期的票增加CSS。
第224行: 第334行:
* 將開票人的資訊放到Action欄位裡,比較好確認這張票的情況。
* 將開票人的資訊放到Action欄位裡,比較好確認這張票的情況。
* 將日曆選擇器中,關閉動畫效果<ref name="jqueryui-datepicker">{{Cite web|url=https://api.jqueryui.com/datepicker/|title=Datepicker Widget &#124; jQuery UI API Documentation|accessdate=2018-02-28}}</ref>,另外將每週的第一天設為星期天<ref name="jqueryui-datepicker"/>,並且允許選擇其他月份的日期<ref name="jqueryui-datepicker"/>。
* 將日曆選擇器中,關閉動畫效果<ref name="jqueryui-datepicker">{{Cite web|url=https://api.jqueryui.com/datepicker/|title=Datepicker Widget &#124; jQuery UI API Documentation|accessdate=2018-02-28}}</ref>,另外將每週的第一天設為星期天<ref name="jqueryui-datepicker"/>,並且允許選擇其他月份的日期<ref name="jqueryui-datepicker"/>。
* 對所有<code>button</code>都設定accesskey。


<syntaxhighlight lang="html">
<syntaxhighlight lang="html">
    <script>
<script>
    <!--
<!--
    // Run immediately.
'use strict';
    (function($) {
 
        // Due date css handling
// Run immediately.
        var d = new Date();
(function($) {
        var today = (new Date(d.getTime() - d.getTimezoneOffset() * 60000)).toISOString().slice(0, 10);
    // Due date css handling
        document.querySelectorAll('table.tickets td.due_date').forEach(function(el) {
    let d = new Date();
            var due = el.innerText.trim();
    let today = (new Date(d.getTime() - d.getTimezoneOffset() * 60000)).toISOString().slice(0, 10);
            if (due < today) {
    document.querySelectorAll('table.tickets td.due_date').forEach(function(el) {
                el.parentElement.classList.add('duedate_overdue');
        let due = el.innerText.trim();
            } else if (due === today) {
        if (due < today) {
                el.parentElement.classList.add('duedate_today');
            el.parentElement.classList.add('duedate_overdue');
        } else if (due === today) {
            el.parentElement.classList.add('duedate_today');
        }
    });
 
    if (document.getElementById('ticketbox')?.style.display === '') {
        // Due date postpone button
        const pptoday = $('<input type="button" value="Postpone (today)" />');
        pptoday.click(function(){
            const duedate = $('#field-due_date');
            let t = duedate.val();
 
            if ('Z' === t.slice(-1)) {
                t = new Date(Date.now());
                t = strftime('%Y-%m-%dT00:00:00Z', t);
                duedate.val(t);
             }
             }
            const 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 = strftime('%Y-%m-%d' + new_str, t);
                duedate.val(t);
            }
            $('#propertyform input[name="submit"]').click();
         });
         });
        $('#action').append(pptoday);


         // Due date (newticket) set to 00:00:00 (in UTC)
         const ppday = $('<input type="button" value="Postpone (day)" />');
        if ($('form[action$="/newticket#ticket"]').length > 0) {
        ppday.click(function(){
             do {
             const duedate = $('#field-due_date');
                if ($('#ticket:visible').hasClass('ticketdraft')) {
            let t = duedate.val();
                    break;
                }


                 let el = $('#field-due_date');
            if ('Z' === t.slice(-1)) {
                 let t = el.val();
                 t = new Date(Date.now());
                t.setDate(t.getDate() + 1);
                t = strftime('%Y-%m-%dT00:00:00Z', t);
                 duedate.val(t);
            }


                if ('Z' === t.slice(-1)) {
            const a = t.match(/\+(\d\d:\d\d)$/);
                    console.log(t);
            if (null !== a) {
                    t = new Date(Date.parse(t) + 86400000);
                let tz = a[1];
                    t = strftime('%Y-%m-%dT00:00:00Z', t);
                let new_str = 'T' + tz + ':00+' + tz;
                    el.val(t);
                t = new Date(Date.now());
                    break;
                t.setDate(t.getDate() + 1);
                }
                t = strftime('%Y-%m-%d' + new_str, t);
                duedate.val(t);
            }


                let a = t.match(/\+(\d\d:\d\d)$/);
            $('#propertyform input[name="submit"]').click();
                if (null !== a) {
        });
                    let tz = a[1];
        $('#action').append(ppday);
                    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
         const ppmonth = $('<input type="button" value="Postpone (month)" />');
        let el = $('<button>Postpone (month)</button>');
         ppmonth.click(function(){
         el.click(function(){
             const duedate = $('#field-due_date');
             let el = $('#field-due_date');
             let t = duedate.val();
             let t = el.val();


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


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


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


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


            var that = this;
        $(this).closest('div').append(b);
            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();


         // Resolve with Buttons
         var b = $('<button/>').click(function(e){
        $('#action_resolve_resolve_resolution option').each(function(){
             var v = $(this).val();
             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);


            var b = $('<button/>').click( function(e){
        $(this).closest('div').append(b);
                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);
        });


         // Closed tickets handling
    // Run after content loaded.
         $('tr:has(a.closed)').addClass('ticket_closed');
    $(function() {
         // Reset Drag & Drop hovering issue.
         let el = document.querySelector('.tracdragdrop-paste.beautytips');
        el?.parentElement.addEventListener('mouseover', ev => {ev.stopPropagation();}, true);


         // Copy reporter information to action section
         // Layout
         if ($('#action_resolve_resolve_resolution').length > 0) {
         $('#attachments').removeClass('collapsed');
            var reporter = $('#h_reporter').parent().find('.trac-author, .trac-author-user')[0].outerHTML;
        $('#modify').parent().removeClass('collapsed');


            $('#action_resolve_resolve_resolution').each(function(){
        // Datepicker
                $(this).parent().append('<span class="hint">(This ticket is created by ' + reporter + ')</span>');
        if ($.datepicker) {
            $.datepicker.setDefaults({
                firstDay: 0,
                selectOtherMonths: true,
                showAnim: '',
                showOtherMonths: true
             });
             });
         }
         }


         // Run after content loaded.
         // Add accesskey
         $(function() {
         $('button').each(function() {
             // Layout
             var el = $(this);
            $('#attachments').removeClass('collapsed');
 
             $('#modify').parent().removeClass('collapsed');
             var a = el.text()[0].toLowerCase();
            el.attr('accesskey', a);
        });


            // Datepicker
        // Add submit accesskey
            if ($.datepicker) {
        $('input[name="submit"]').each(function() {
                $.datepicker.setDefaults({
            $(this).attr('accesskey', 'c');
                    firstDay: 0,
                    selectOtherMonths: true,
                    showAnim: '',
                    showOtherMonths: true
                });
            }
         });
         });
    })(jQuery);
    //-->
    </script>
</syntaxhighlight>


==== htdoc/style.css ====
        // Closed tickets handling
* 全部使用sans-serif字型。
        $('tr:has(a.closed)').addClass('ticket_closed');
* 修正button因為CSS效果而有時會按不到的問題。
    });
* 針對code block內一行過長時自動換行。
})(jQuery);
* 針對今天到期與過期的票用不同的標示標出(配合下方的JavaScript)。
* 將已經關掉的票變淡(配合下方的JavaScript)。
* 讓編輯區域使用等寬字型。
* 讓可用範圍變寬。
* 讓圖片不要超過螢幕寬度。


<syntaxhighlight lang="css">
//-->
    body, th, tr {
</script>
        font-family: sans-serif;
    }
    input[type=button]:active, input[type=submit]:active,
    input[type=reset]:active {
        position: relative;
        top: 0;
        left: 0;
    }
    pre {
        white-space: pre-wrap;
    }
    table.tickets tr.duedate_overdue {
        font-weight: bold;
    }
    table.tickets tr.duedate_today {
        border: 2px solid;
    }
    table.subtickets tr.ticket_closed, .closed.ticket {
        opacity: 0.5;
    }
    textarea {
        font-family: monospace;
    }
    #content.ticket {
        width: 78em;
    }
    #main img {
        max-width: 100%;
    }
</syntaxhighlight>
</syntaxhighlight>


第430行: 第527行:
* nginx
* nginx
* OpenVPN
* OpenVPN
* Tor
* Tor(2020年六月終止<ref>{{Cite web |url=https://blog.torproject.org/node/1957 |title=From Trac into Gitlab for Tor |accessdate=2020-11-22 |date=2020-11-20 |language=en}}</ref>)
* VirtualBox
* VirtualBox
* WebKit
* WebKit

於 2024年12月20日 (五) 18:24 的最新修訂

Trac是一套問題追蹤系統英語:Issue tracking system)。

簡介

優點

  • 簡單,專注在事情的記錄上,而不是限制行為上。

缺點

  • 目前的版本(1.4)還是不支援Python 3,但1.5已經在開發了。目前支援Python 3的1.6版已經出了。

安裝

我是將Python裝在service-trac這個使用者的pyenv裡面(權限會是service-trac:service-trac):

sudo useradd service-trac -m -r -s /bin/bash

在裝完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 TracRobotsTxt
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/httpauthplugin/trunk/
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/httpauthplugin/trunk/
-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
TracHTTPAuth
TracRobotsTxt
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');
        }
    });

    if (document.getElementById('ticketbox')?.style.display === '') {
        // Due date postpone button
        const pptoday = $('<input type="button" value="Postpone (today)" />');
        pptoday.click(function(){
            const duedate = $('#field-due_date');
            let t = duedate.val();

            if ('Z' === t.slice(-1)) {
                t = new Date(Date.now());
                t = strftime('%Y-%m-%dT00:00:00Z', t);
                duedate.val(t);
            }

            const 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 = strftime('%Y-%m-%d' + new_str, t);
                duedate.val(t);
            }

            $('#propertyform input[name="submit"]').click();
        });
        $('#action').append(pptoday);

        const ppday = $('<input type="button" value="Postpone (day)" />');
        ppday.click(function(){
            const duedate = $('#field-due_date');
            let t = duedate.val();

            if ('Z' === t.slice(-1)) {
                t = new Date(Date.now());
                t.setDate(t.getDate() + 1);
                t = strftime('%Y-%m-%dT00:00:00Z', t);
                duedate.val(t);
            }

            const 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(t.getDate() + 1);
                t = strftime('%Y-%m-%d' + new_str, t);
                duedate.val(t);
            }

            $('#propertyform input[name="submit"]').click();
        });
        $('#action').append(ppday);

        const ppmonth = $('<input type="button" value="Postpone (month)" />');
        ppmonth.click(function(){
            const duedate = $('#field-due_date');
            let t = duedate.val();

            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);
                duedate.val(t);
            }

            const 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);
                duedate.val(t);
            }

            $('#propertyform input[name="submit"]').click();
        });
        $('#action').append(ppmonth);
    }

    // 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). 

外部連結