「Trac」:修訂間差異

出自Gea-Suan Lin's Wiki
跳至導覽 跳至搜尋
本頁面具有訪問限制。如果您看見此訊息,這代表您沒有訪問本頁面的權限。
(未顯示同一使用者於中間所作的 36 次修訂)
行 10: 行 10:
== 安裝 ==
== 安裝 ==
 我是將[[Python]]裝在<code>www-data</code>這個使用者的[[pyenv]]裡面(權限會是<code>www-data:www-data</code>)。在裝完Python後,用<code>pip</code>安裝以下套件:
 我是將[[Python]]裝在<code>www-data</code>這個使用者的[[pyenv]]裡面(權限會是<code>www-data:www-data</code>)。在裝完Python後,用<code>pip</code>安裝以下套件:
<syntaxhighlight lang="shell-session">
 
$ pip install Trac
<syntaxhighlight lang="bash">
$ pip install ​Pygments
sudo apt install -y libmysqlclient-dev
$ pip install mysql-python
pip install mysql-python 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]]作為後端資料庫時,建議用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`@`localhost` IDENTIFIED BY 'password_here';</syntaxhighlight>


 這 在安裝Trac時的連線設定會是<code>mysql://trac:password_here@localhost/trac</code>(需要自己修改對應欄位)。
<syntaxhighlight lang="sql">
CREATE DATABASE trac DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
GRANT ALL ON trac.* TO `trac`@`localhost` IDENTIFIED BY 'password_here';
</syntaxhighlight>
 
接下來就可以產生環境:
 
<syntaxhighlight lang="bash">
cd /srv/issue.example.com
trac-admin trac initenv
</syntaxhighlight>
 
 這 在安裝Trac時的 資料庫 連線設定會是<code>mysql://trac:password_here@localhost/trac</code>(需要自己修改對應欄位)。


 在安裝套件時,軟體本體常常會放在[[Subversion]]的伺服器內,在透過<code>pip</code>或是<code>easy_install</code>時會呼叫Subversion,所以會需要安裝對應的軟體:
 在安裝套件時,軟體本體常常會放在[[Subversion]]的伺服器內,在透過<code>pip</code>或是<code>easy_install</code>時會呼叫Subversion,所以會需要安裝對應的軟體:
<syntaxhighlight lang="shell-session">
 
# apt install subversion
<syntaxhighlight lang="bash">
sudo apt install -y subversion
</syntaxhighlight>
</syntaxhighlight>


 另外要建立目錄,並且讓Trac可以寫入:
 另外要建立目錄,並且讓Trac可以寫入:
<syntaxhighlight lang="shell-session">
 
# mkdir trac/files
<syntaxhighlight lang="bash">
# chown www-data:www-data trac/files
sudo mkdir trac/files
sudo chown www-data:www-data trac/files
</syntaxhighlight>
</syntaxhighlight>


 然後是Trac的[[FastCGI]]檔案,放在Trac的project目錄下,並且用<code>chmod 755 trac.fcgi</code>改成可執行:
 然後是Trac的[[FastCGI]]檔案,放在Trac的project目錄下,並且用<code>chmod 755 trac.fcgi</code>改成可執行:
<syntaxhighlight lang="python">
<syntaxhighlight lang="python">
#!/usr/bin/env python
#!/usr/bin/env python
行 56: 行 69:
</syntaxhighlight>
</syntaxhighlight>


 另外[[Systemd]]的設定檔讓Trac在開機時以FastCGI模式跑起來,這個檔案放在<code>/lib/systemd/system/rsyslog.service</code>並連結到<code>/etc/systemd/system/multi-user.target.wants/trac.service</code> (要記得把裡面的<code>/srv/trac.example.com/trac/</code>改成自己的目錄</code>:
 另外[[Systemd]]的設定檔讓Trac在開機時以FastCGI模式跑起來,這個檔案放在<code>/lib/systemd/system/trac-fcgi.service</code>(要記得把裡面的<code>/srv/trac.example.com/trac/</code>改成自己的目錄</code>:
 
<syntaxhighlight lang="ini">
<syntaxhighlight lang="ini">
[Unit]
[Unit]
行 62: 行 76:


[Service]
[Service]
ExecStart=/bin/bash -l -c "exec /srv/trac.gslin.com/trac/trac.fcgi"
ExecStart=/bin/bash -l -c "exec /srv/issue.example.com/trac/trac.fcgi"
Group=www-data
Group=www-data
RuntimeDirectory=trac
RuntimeDirectory=trac
行 70: 行 84:
[Install]
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target
</syntaxhighlight>
然後讓systemd重新讀取,並且設定為開機啟動,最後手動先跑起來(或是重開機):
<syntaxhighlight lang="bash">
sudo systemctl daemon-reload
sudo systemctl enable trac-fcgi
sudo service trac-fcgi restart
</syntaxhighlight>
</syntaxhighlight>


行 88: 行 110:
** 配合AccountManagerPlugin與XmlRpcPlugin使用的,當AccountManagerPlugin啟用HTML Form Login時,XMLRPC會因為不支援這種登入方式而失效,而這個套件可以解這個問題。
** 配合AccountManagerPlugin與XmlRpcPlugin使用的,當AccountManagerPlugin啟用HTML Form Login時,XMLRPC會因為不支援這種登入方式而失效,而這個套件可以解這個問題。
* [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>套件
* [https://trac-hacks.org/wiki/SubticketsPlugin SubticketsPlugin – Trac Hacks - Plugins Macros etc.]
* [https://trac-hacks.org/wiki/SubticketsPlugin SubticketsPlugin – Trac Hacks - Plugins Macros etc.]
** 子母票的延伸套件,基本上是必備項目。
** 子母票的延伸套件,基本上是必備項目。
行 111: 行 133:
pip install svn+https://trac-hacks.org/svn/httpauthplugin/trunk/
pip install svn+https://trac-hacks.org/svn/httpauthplugin/trunk/
pip install git+https://github.com/wagnerpinheiro/trac-slack-plugin.git
pip install git+https://github.com/wagnerpinheiro/trac-slack-plugin.git
pip install requests
pip install git+https://github.com/trac-hacks/trac-subtickets-plugin.git
pip install git+https://github.com/trac-hacks/trac-subtickets-plugin.git
pip install TracCronPlugin
pip install TracCronPlugin
行 131: 行 154:
TracCronPlugin
TracCronPlugin
TracXMLRPC
TracXMLRPC
requests
</syntaxhighlight>
</syntaxhighlight>
可以用<code>pip install -r requirements.txt</code>安裝。


=== 曾經裝過的套件 ===
=== 曾經裝過的套件 ===
行 145: 行 171:
* [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列表內的人可以讀。
行 155: 行 183:


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


行 161: 行 191:
* <code>TICKET_EDIT_DESCRIPTION</code>
* <code>TICKET_EDIT_DESCRIPTION</code>


=== trac.ini ===
=== conf/trac.ini ===
 
 我自己的Report不想分頁(預設是一頁100筆),所以設為:
 我自己的Report不想分頁(預設是一頁100筆),所以設為:
<syntaxhighlight lang="ini">
<syntaxhighlight lang="ini">
[report]
[report]
行 168: 行 200:
</syntaxhighlight>
</syntaxhighlight>


  讓系統吃<code>code.jquery.com</code> 提供的jQuery 及jQuery UI<ref name="tracini">{{Cite web|url=https://trac.edgewall.org/wiki/TracIni|title=TracIni   – The Trac Project|accessdate=2018-02-28}}</ref>,稍微降低伺服器 負載,另外也有機會與外部網站共用cache。目前的Trac 1.2.2版本可以這樣設
  另外因為我們不太用Trac內建的Wiki,但又關不掉, 所以 只能針對沒找到 頁面就不要產生連結了
<syntaxhighlight lang="ini">
[trac]
jquery_location = https://code.jquery.com/jquery-1.12.4.min.js
jquery_ui_location = https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
jquery_ui_theme_location = https://code.jquery.com/ui/1.12.1/themes/start/jquery-ui.css
</syntaxhighlight>


另外因為我們不太用Trac內建的Wiki,但又關不掉,所以只能針對沒找到的頁面就不要產生連結了:
<syntaxhighlight lang="ini">
<syntaxhighlight lang="ini">
[wiki]
[wiki]
行 182: 行 207:
</syntaxhighlight>
</syntaxhighlight>


=== site.html ===
=== htdoc/style.css ===
在<code>templates/site.html</code>裡做了一些事情進行客製化。
 
對於外部引用,不要 洩漏<code>Referrer</code>:
<syntaxhighlight lang="html">
<head>
   <meta name="referrer" content="no-referrer" />
   ...
</head>
</syntaxhighlight>


==== CSS ====
* 全部使用sans-serif字型。
* 修正button因為CSS效果而有時會按不到的問題。
* 修正button因為CSS效果而有時會按不到的問題。
* 針對code block內一行過長時自動換行。
* 針對code block內一行過長時自動換行。
行 202: 行 216:
* 讓可用範圍變寬。
* 讓可用範圍變寬。
* 讓圖片不要超過螢幕寬度。
* 讓圖片不要超過螢幕寬度。
<syntaxhighlight lang="css">
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 td:last-of-type {
   width: 15em;
}
table.subtickets tr.ticket_closed, .closed.ticket {
   opacity: 0.5;
}
textarea {
   font-family: monospace;
}
#content.ticket {
   width: 78em;
}
#main img {
   max-width: 100%;
}
</syntaxhighlight>
=== template/site_head.html ===
這是Trac 1.4的Jinja2設計,如果是Trac 1.2或更早版本的使用者,可以在<code>template/site.html</code>裡面增加。


<syntaxhighlight lang="html">
<syntaxhighlight lang="html">
   <style type="text/css">
<meta name="referrer" content="no-referrer" />
   <!--
<link rel="stylesheet" href="${href.chrome('site/style.css')}" />
   body, th, tr {
<script src="https://cdn.jsdelivr.net/npm/strftime@0.10.0/strftime.min.js"></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%;
   }
   -->
   </style>
</syntaxhighlight>
</syntaxhighlight>


==== JavaScript ====
=== 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 &#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。


在<code>head</code>的地方先加入<code>strftime</code>套件,後面會用到:
<syntaxhighlight lang="html">
<syntaxhighlight lang="html">
   <script src="https://cdn.jsdelivr.net/npm/strftime@0.10.0/strftime.min.js"></script>
<script>
</syntaxhighlight>
<!--
'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;
       }


這段程式碼則會放在<code></body></code>結尾前:
       let a = t.match(/\+(\d\d:\d\d)$/);
<syntaxhighlight lang="html">
       if (null !== a) {
   <script>
         let tz = a[1];
   <!--
         let new_str = 'T' + tz + ':00+' + tz;
   // Run immediately.
         t = new Date(Date.parse(t) + 86400000);
   (function($) {
         t = strftime('%Y-%m-%d' + new_str, t);
     // Due date css handling
          el.val(t);
     var d = new Date();
          break;
     var today = (new Date(d.getTime() - d.getTimezoneOffset() * 60000)).toISOString().slice(0, 10);
     document.querySelectorAll('table.tickets td.due_date').forEach(function(el) {
       var due = el.innerText.trim();
       if (due < today) {
          el.parentElement.classList.add('duedate_overdue');
       } else if (due === today) {
          el.parentElement.classList.add('duedate_today');
        }
        }
      });
      } while (false);
   }


     // Due date (newticket) set to 00:00:00 (in UTC)
   // Due date postpone button
     if ($('form[action$="/newticket#ticket"]').length > 0) {
   let el = $('<button>Postpone (month)</button>');
       do {
   el.click(function(){
         let el = $('#field-due_date');
     let el = $('#field-due_date');
         let t = el.val();
     let t = el.val();


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


         let a = t.match(/\+(\d\d:\d\d)$/);
       let 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.parse(t) + 86400000);
         t = new Date(Date.now());
           t = strftime('%Y-%m-%d' + new_str, t);
         t.setDate(1);
           el.val(t);
         t.setMonth(t.getMonth() + 2);
           break;
         t.setDate(0);
         }
         t = strftime('%Y-%m-%d' + new_str, t);
       } while (false);
         el.val(t);
     }
         break;
       }
     } while (false);


     // Accept & start buttons
      $('#propertyform input[name="submit"]').click();
      $('#action_accept, #action_start').each(function(){
   });
       var v = $(this).val();
   });
   $('td[headers="h_due_date"]').append(el);


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


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


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


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


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


     // Closed tickets handling
      $(this).closest('div').append(b);
      $('tr:has(a.closed)').addClass('ticket_closed');
   });


     // Copy reporter information to action section
   // Closed tickets handling
     if ($('#action_resolve_resolve_resolution').length > 0) {
   $('tr:has(a.closed)').addClass('ticket_closed');
       var reporter = $('#h_reporter').parent().find('.trac-author, .trac-author-user')[0].outerHTML;
 
       $('#action_resolve_resolve_resolution').each(function(){
         $(this).parent().append('<span class="hint">(This ticket is created by ' + reporter + ')</span>');
       });
     }
   })(jQuery);


    // Run after content loaded.
    // Run after content loaded.
    jQuery(function() {
    $(function() {
      // Layout
      // Layout
      jQuery('#attachments').removeClass('collapsed');
      $('#attachments').removeClass('collapsed');
      jQuery('#modify').parent().removeClass('collapsed');
      $('#modify').parent().removeClass('collapsed');


      // Datepicker
      // Datepicker
行 358: 行 408:
        });
        });
      }
      }
     // Add accesskey
     $('button').each(function() {
       var el = $(this);
       var a = el.text()[0].toLowerCase();
       el.attr('accesskey', a);
     });
    });
    });
   //-->
})(jQuery);
   </script>
 
//-->
</script>
</syntaxhighlight>
</syntaxhighlight>


行 373: 行 433:
* Django
* Django
* FFmpeg
* FFmpeg
* jQuery
* MacPorts
* nginx
* nginx
* OpenVPN
* Tor
* Tor
* VirtualBox
* WebKit
* WebKit
* WordPress
* WordPress
* jQuery


== 相關連結 ==
== 相關連結 ==

於 2020年1月13日 (一) 23:32 的修訂

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

簡介

優點

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

缺點

  • 目前還是不支援Python 3。

安裝

我是將Python裝在www-data這個使用者的pyenv裡面(權限會是www-data:www-data)。在裝完Python後,用pip安裝以下套件:

sudo apt install -y libmysqlclient-dev
pip install mysql-python PyMySQL Pygments Trac

使用MySQL作為後端資料庫時,建議用utf8mb4作為基礎,這樣資料庫可以存所有範圍的Unicode字元[1],另外建立獨立的使用者供Trac使用:

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

接下來就可以產生環境:

cd /srv/issue.example.com
trac-admin trac initenv

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

在安裝套件時,軟體本體常常會放在Subversion的伺服器內,在透過pip或是easy_install時會呼叫Subversion,所以會需要安裝對應的軟體:

sudo apt install -y subversion

另外要建立目錄,並且讓Trac可以寫入:

sudo mkdir trac/files
sudo chown www-data:www-data trac/files

然後是Trac的FastCGI檔案,放在Trac的project目錄下,並且用chmod 755 trac.fcgi改成可執行:

#!/usr/bin/env python

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

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

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

num -= 1
while num > 0:
    num -= 1
    if 0 == os.fork():
        break

fcgiserv.run()

另外Systemd的設定檔讓Trac在開機時以FastCGI模式跑起來,這個檔案放在/lib/systemd/system/trac-fcgi.service(要記得把裡面的/srv/trac.example.com/trac/改成自己的目錄:

[Unit]
Description=trac fcgi daemon

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

[Install]
WantedBy=multi-user.target

然後讓systemd重新讀取,並且設定為開機啟動,最後手動先跑起來(或是重開機):

sudo systemctl daemon-reload
sudo systemctl enable trac-fcgi
sudo service trac-fcgi restart

套件

目前有安裝的套件

除了基本安裝外,還會安裝這些套件:

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

#!/bin/bash
pip install TracAccountManager
pip install svn+https://trac-hacks.org/svn/duplicateticketsearchplugin/trunk/
pip install svn+https://trac-hacks.org/svn/graphvizplugin/branches/1.2/
pip install svn+https://trac-hacks.org/svn/httpauthplugin/trunk/
pip install git+https://github.com/wagnerpinheiro/trac-slack-plugin.git
pip install requests
pip install git+https://github.com/trac-hacks/trac-subtickets-plugin.git
pip install TracCronPlugin
pip install svn+https://trac-hacks.org/svn/tracdragdropplugin/0.12/
pip install svn+https://trac-hacks.org/svn/tracwysiwygplugin/0.12/
pip install TracXMLRPC

對應的requirements.txt是:

-e git+https://github.com/trac-hacks/trac-subtickets-plugin.git
-e git+https://github.com/wagnerpinheiro/trac-slack-plugin.git
-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/
TracAccountManager
TracCronPlugin
TracXMLRPC
requests

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

曾經裝過的套件

以前會安裝,但現在因為自己用而沒有裝上(沒有需求或是不想裝):

設定

權限

Trac預設的權限設定過於嚴格,只給內部使用的情境下,建議對authenticated群組加上:

  • TICKET_EDIT_CC
  • TICKET_EDIT_DESCRIPTION

conf/trac.ini

我自己的Report不想分頁(預設是一頁100筆),所以設為:

[report]
items_per_page = 0

另外因為我們不太用Trac內建的Wiki,但又關不掉,所以只能針對沒找到的頁面就不要產生連結了:

[wiki]
ignore_missing_pages = enabled

htdoc/style.css

  • 修正button因為CSS效果而有時會按不到的問題。
  • 針對code block內一行過長時自動換行。
  • 針對今天到期與過期的票用不同的標示標出(配合下方的JavaScript)。
  • 將已經關掉的票變淡(配合下方的JavaScript)。
  • 讓編輯區域使用等寬字型。
  • 讓可用範圍變寬。
  • 讓圖片不要超過螢幕寬度。
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 td:last-of-type {
    width: 15em;
}
table.subtickets tr.ticket_closed, .closed.ticket {
    opacity: 0.5;
}
textarea {
    font-family: monospace;
}
#content.ticket {
    width: 78em;
}
#main img {
    max-width: 100%;
}

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);
    });

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

    // Run after content loaded.
    $(function() {
        // 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);
        });
    });
})(jQuery);

//-->
</script>

其他

刪除使用者時可能會需要手動到以下表格清除資料(清完後需要重跑Trac,因為Trac會讀出來cache一份),避免autocomplete之類的套件仍然顯示:

  • auth_cookie
  • session
  • session_attribute

著名使用單位

  • Django
  • FFmpeg
  • jQuery
  • MacPorts
  • nginx
  • OpenVPN
  • Tor
  • VirtualBox
  • WebKit
  • WordPress

相關連結

參考資料

外部連結