「Trac」:修訂間差異
(未顯示同一使用者於中間所作的 102 次修訂) | |||
第2行: | 第2行: | ||
== 簡介 == | == 簡介 == | ||
=== 優點 === | === 優點 === | ||
* 簡單,專注在事情的記錄上,而不是限制行為上。 | * 簡單,專注在事情的記錄上,而不是限制行為上。 | ||
=== 缺點 === | === 缺點 === | ||
* | |||
* <del>目前的版本(1.4)還是不支援Python 3,但1.5已經在開發了。</del>目前支援[[Python]] 3的1.6版已經出了。 | |||
== 安裝 == | == 安裝 == | ||
我是將[[Python]]裝在<code> | |||
<syntaxhighlight lang=" | 我是將[[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"> | |||
sudo apt install -y libmysqlclient-dev | |||
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">CREATE DATABASE trac DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; | |||
GRANT ALL ON trac.* TO `trac`@` | <syntaxhighlight lang="sql"> | ||
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`; | |||
</syntaxhighlight> | |||
這 | 接下來就可以產生環境: | ||
<syntaxhighlight lang="bash"> | |||
cd ~; trac-admin trac initenv | |||
</syntaxhighlight> | |||
這邊在安裝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,所以會需要安裝對應的軟體: | ||
<syntaxhighlight lang=" | |||
<syntaxhighlight lang="bash"> | |||
sudo apt install -y subversion | |||
</syntaxhighlight> | </syntaxhighlight> | ||
另外要建立目錄 | 另外要建立目錄(放附件之類的東西): | ||
<syntaxhighlight lang=" | |||
<syntaxhighlight lang="bash"> | |||
cd ~; mkdir 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 | #!/usr/bin/env python3 | ||
import os | import os | ||
第41行: | 第72行: | ||
import trac.web._fcgi | import trac.web._fcgi | ||
num = | num = 4 | ||
sockaddr = ' | 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 - | for _ in range(num - 1): | ||
if 0 == os.fork(): | if 0 == os.fork(): | ||
break | break | ||
第56行: | 第85行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
另外[[ | 另外使用userland [[systemd]](需要先啟用這個設定),讓Trac在開機時將FastCGI daemon跑起來,這個檔案放在<code>~/.config/systemd/user/trac-fcgi.service</code>: | ||
<syntaxhighlight lang="ini"> | <syntaxhighlight lang="ini"> | ||
[Unit] | [Unit] | ||
Description= | Description=Trac FastCGI daemon | ||
After=remote-fs.target | |||
[Service] | [Service] | ||
ExecStart=/bin/bash -l -c "exec | ExecStart=/bin/bash -l -c "exec ~/trac/trac.fcgi" | ||
Type=simple | Type=simple | ||
[Install] | [Install] | ||
WantedBy= | WantedBy=default.target | ||
</syntaxhighlight> | |||
然後讓systemd重新讀取,並且設定為開機啟動,最後手動先跑起來(或是重開機): | |||
<syntaxhighlight lang="bash"> | |||
systemctl --user daemon-reload | |||
systemctl --user enable --now trac-fcgi.service | |||
</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> | ||
第77行: | 第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。 | ||
第85行: | 第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 | * [https://trac-hacks.org/wiki/HttpAuthPlugin HttpAuthPlugin] | ||
** | ** 將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上。 | ** 可以把變更丟到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用的。 | ||
第102行: | 第146行: | ||
** 讓使用者可以透過API操作Trac。 | ** 讓使用者可以透過API操作Trac。 | ||
目前Trac 1. | 目前Trac 1.4版的安裝方式是: | ||
<syntaxhighlight lang="bash"> | <syntaxhighlight lang="bash"> | ||
#!/bin/bash | #!/bin/bash | ||
pip install | 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/ | 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/ | |||
</syntaxhighlight> | |||
對應的<code>requirements.txt</code>是: | |||
<syntaxhighlight lang="text"> | |||
-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 | |||
</syntaxhighlight> | </syntaxhighlight> | ||
可以用<code>pip install -r requirements.txt</code>安裝。 | |||
=== 曾經裝過的套件 === | === 曾經裝過的套件 === | ||
第129行: | 第198行: | ||
* [https://trac-hacks.org/wiki/LDAPAcctMngrPlugin LDAPAcctMngrPlugin – Trac Hacks - Plugins Macros etc.] | * [https://trac-hacks.org/wiki/LDAPAcctMngrPlugin LDAPAcctMngrPlugin – Trac Hacks - Plugins Macros etc.] | ||
** 可以使用[[LDAP]]管理帳號,是AccountManagerPlugin的延伸套件。 | ** 可以使用[[LDAP]]管理帳號,是AccountManagerPlugin的延伸套件。 | ||
** 要注意在文件上沒有提到需要安裝Pythoh module,<code>ldap</code>與<code>python-ldap</code>,需要自己手動裝。 | ** 要注意在文件上沒有提到需要安裝Pythoh module,<code>ldap</code>與<code>python-ldap</code>,需要自己手動裝。(這兩個模組需要一些系統的套件,可以透過<code>sudo apt install -y libsasl2-dev python-dev libldap2-dev libssl-dev</code>安裝) | ||
* [https://github.com/gslin/trac-addtocc-plugin gslin/trac-addtocc-plugin: Add participator to cc list automatically.] | * [https://github.com/gslin/trac-addtocc-plugin gslin/trac-addtocc-plugin: Add participator to cc list automatically.] | ||
** 自動把參與者加到Cc列表內,這樣才會收到後續的更新。 | ** 自動把參與者加到Cc列表內,這樣才會收到後續的更新。 | ||
* [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>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列表內的人可以讀。 | ||
第139行: | 第210行: | ||
== 設定 == | == 設定 == | ||
=== 權限 === | === 權限 === | ||
透過CLI的方式可以更新權限: | |||
<syntaxhighlight lang="bash"> | |||
trac-admin /path/to/myproject permission add admin TRAC_ADMIN | |||
</syntaxhighlight> | |||
Trac預設的權限設定過於嚴格,只給內部使用的情境下,建議對<code>authenticated</code>群組加上: | Trac預設的權限設定過於嚴格,只給內部使用的情境下,建議對<code>authenticated</code>群組加上: | ||
第145行: | 第224行: | ||
* <code>TICKET_EDIT_DESCRIPTION</code> | * <code>TICKET_EDIT_DESCRIPTION</code> | ||
=== 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筆),所以設為: | ||
<syntaxhighlight lang="ini"> | <syntaxhighlight lang="ini"> | ||
[report] | [report] | ||
第152行: | 第253行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
另外因為我們不太用Trac內建的Wiki,但又關不掉,所以只能針對沒找到的頁面就不要產生連結了: | |||
<syntaxhighlight lang="ini"> | <syntaxhighlight lang="ini"> | ||
[wiki] | [wiki] | ||
第166行: | 第260行: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=== | === htdoc/style.css === | ||
* 修正button因為CSS效果而有時會按不到的問題。 | * 修正button因為CSS效果而有時會按不到的問題。 | ||
* 針對code block內一行過長時自動換行。 | * 針對code block內一行過長時自動換行。 | ||
第184行: | 第267行: | ||
* 將已經關掉的票變淡(配合下方的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> | |||
=== template/site_head.html === | |||
這是Trac 1.4的Jinja2設計,如果是Trac 1.2或更早版本的使用者,可以在<code>template/site.html</code>裡面增加。 | |||
<syntaxhighlight lang="html"> | <syntaxhighlight lang="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> | |||
</syntaxhighlight> | </syntaxhighlight> | ||
=== | === template/site_footer.html === | ||
這是Trac 1.4的Jinja2設計,如果是Trac 1.2或更早版本的使用者,可以在<code>template/site.html</code>裡面增加。 | |||
* 用JavaScript針對今天到期以及過期的票增加CSS。 | * 用JavaScript針對今天到期以及過期的票增加CSS。 | ||
* 將新票裡的Due Date改為零點零分零秒。 | * 將新票裡的Due Date改為零點零分零秒。 | ||
* 增加把票直接延到下個月月底的button。 | |||
* 增加button,讓關票可以直接點擊。 | * 增加button,讓關票可以直接點擊。 | ||
* 將票裡的attachments與modify內容展開。 | * 將票裡的attachments與modify內容展開。 | ||
* 將開票人的資訊放到Action欄位裡,比較好確認這張票的情況。 | * 將開票人的資訊放到Action欄位裡,比較好確認這張票的情況。 | ||
* 將日曆選擇器中,關閉動畫效果<ref name="jqueryui-datepicker">{{Cite web|url=https://api.jqueryui.com/datepicker/|title=Datepicker Widget | 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 | 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> | |||
</ | <!-- | ||
'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(); | 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(); | |||
$(this). | 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 | // Layout | ||
$('#attachments').removeClass('collapsed'); | |||
$('#modify').parent().removeClass('collapsed'); | |||
// Datepicker | // Datepicker | ||
第342行: | 第490行: | ||
}); | }); | ||
} | } | ||
// 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> | |||
</syntaxhighlight> | </syntaxhighlight> | ||
第357行: | 第523行: | ||
* Django | * Django | ||
* FFmpeg | * FFmpeg | ||
* jQuery | |||
* MacPorts | |||
* nginx | * nginx | ||
* Tor | * OpenVPN | ||
* 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 | |||
* WebKit | * WebKit | ||
* WordPress | * WordPress | ||
== 相關連結 == | == 相關連結 == |
於 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
套件
目前有安裝的套件
除了基本安裝外,還會安裝這些套件:
- AccountManagerPlugin – Trac Hacks - Plugins Macros etc.
- 可以吃HTML Form Login,而不侷限在HTTP authentication。
- BlueFlatTheme – Trac Hacks - Plugins Macros etc.
- 目前Trac上少數支援Responsive Design的Theme,需要使用ThemeEnginePlugin安裝並啟用。
- DuplicateTicketSearchPlugin – Trac Hacks - Plugins Macros etc.
- 在開新票時可以拉出有哪些是可能重複的票(尤其是已經關掉的票)。
- GraphvizPlugin – Trac Hacks - Plugins Macros etc.
- Graphviz可以拿來畫各類圖,我自己最常用的是Dot圖。
- HttpAuthPlugin
- 將HTTP header中的Authentication轉成AccountManagerPlugin也可以讀到的欄位,主要是使用AccountManagerPlugin與XmlRpcPlugin時的戶接,PyPI上面的版本太舊已經不相容現在的Trac,請安裝trunk版本。
- RobotsTxtPlugin
- 支援
/robots.txt
,透過Trac的Wiki實作。
- 支援
- SlackIntegration – Trac Hacks - Plugins Macros etc.
- 可以把變更丟到Slack上,這邊要注意的是套件本身沒有把相依性做好,需要自己安裝
requests
套件。在Trac 1.6上面因為Python 3的原因會有問題,可以裝我修的版本。
- 可以把變更丟到Slack上,這邊要注意的是套件本身沒有把相依性做好,需要自己安裝
- SubticketsPlugin – Trac Hacks - Plugins Macros etc.
- 子母票的延伸套件,基本上是必備項目。原版的程式安裝在MySQL 8.0的環境下會出問題(也不支援Trac 1.6),可以用我修的版本。
- ThemeEnginePlugin – Trac Hacks - Plugins Macros etc.
- Theme的套件管理工具,配合BlueFlatTheme用的。
- TracCronPlugin – Trac Hacks - Plugins Macros etc.
- 定時跑任務,但其實沒很好用... 目前主要是拿來每天寄信通知有哪些票過期。
- TracDragDropPlugin – Trac Hacks - Plugins Macros etc.
- 拖拉就可以上傳附件的套件。
- TracWysiwygPlugin – Trac Hacks - Plugins Macros etc.
- 可以針對Textarea區塊使用WYSIWYG界面,對於非技術類的使用者來說會方便不少(因為不需要學Trac的Wiki語法)。另外有不少文件可以直接剪下貼上(雖然還是會掉不少效果,但比起完全不能剪下貼上來說已經方便不少)。
- XmlRpcPlugin – Trac Hacks - Plugins Macros etc.
- 讓使用者可以透過API操作Trac。
目前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
安裝。
曾經裝過的套件
以前會安裝,但現在因為自己用而沒有裝上(沒有需求或是不想裝):
- AutocompleteUsersPlugin – Trac Hacks - Plugins Macros etc.
- 讓使用者帳號名稱可以autocomplete,在Cc欄位與Owner欄位會很好用。
- DefaultCcPlugin – Trac Hacks - Plugins Macros etc.
- 針對不同的Component自動加上Cc列表。
- 對於企業內還蠻有用的。像是專案可以開
Proj.
開頭的Component,而組織可以開Org.
開頭的Component,這些類型可以設定對應的Cc列表。 - 因為新版在更改Component時不會增加Cc列表,但這對於企業其實還蠻好用的,所以我自己包了一個版本在gslin/defaultccplugin這邊。
- LDAPAcctMngrPlugin – Trac Hacks - Plugins Macros etc.
- 可以使用LDAP管理帳號,是AccountManagerPlugin的延伸套件。
- 要注意在文件上沒有提到需要安裝Pythoh module,
ldap
與python-ldap
,需要自己手動裝。(這兩個模組需要一些系統的套件,可以透過sudo apt install -y libsasl2-dev python-dev libldap2-dev libssl-dev
安裝)
- gslin/trac-addtocc-plugin: Add participator to cc list automatically.
- 自動把參與者加到Cc列表內,這樣才會收到後續的更新。
- Add header's Message-ID field to References field for Trac.
- 當使用Amazon SES發信時,會因為
Message-ID
被換掉,而導致Mail Client無法透過References
將同一張票的通知信整在一起。這個套件透過把信件本身的Message-ID
複製一份到References
的workaround讓Mail Client可以正確判斷出這些信件是同一張票。
- 當使用Amazon SES發信時,會因為
- gslin/trac-secret-checkbox-ticket: Add ticket security policy for Trac.
- 將票設定為祕密,只有Reporter(開票人)、Owner(目前有票的人)、Cc列表內的人可以讀。
- 這對於企業內有些需求很好用(像是還在進行中的計畫,或是一些不會對全公司公開的數據)。
- TracTicketReferencePlugin – Trac Hacks - Plugins Macros etc.
- 可以設定相關連的票,不過我很少用到這個功能(更多是子母票)。
設定
權限
透過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>
這是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
相關連結
參考資料
- ↑ MySqlDb – The Trac Project. [2018-03-01].
- ↑ 2.0 2.1 2.2 Datepicker Widget | jQuery UI API Documentation. [2018-02-28].
- ↑ From Trac into Gitlab for Tor. 2020-11-20 [2020-11-22] (English).