2010年5月10日 星期一

用 apache + mongrel cluster + relative url 跑 Redmine

由於公司防火牆設定問題, 想想還是把 Redmine 跑在 apache 裡共用 port 80, 對大家比較方便。今晚就試了一會兒, 沒想到意外設成功了, 真開心啊!!

我大致上是參考這篇。執行方式是 mongrel 自己跑 web server 聽幾個 port, 再用 apache 跑 proxy, 將特定路徑轉交給 mongrel server。摘要做法如下:
  1. 參考隨便一篇文章在 Ubuntu 上裝好 Redmine, 能用單機 mongrel_rails 執行。
  2. 安裝 mongrel_cluster:
    sudo gem install mongrel_cluster
  3. 設 REDMINE_HOME/config/mongrel_cluster.yml:
    user: www-data
    cwd: REDMINE_HOME
    port: "3000"
    environment: production
    group: www-data
    address: 0.0.0.0
    pid_file: log/mongrel.pid
    servers: 2
  4. 設 Redmine 的相對網址, 在 REDMINE_HOME/config/environments/production.rb 檔案最後加上
    config.action_controller.relative_url_root = '/redmine'
    比起 Django 真是簡單百倍。
  5. 執行 mongrel cluster:
    cd REDMINE_HOME
    mongrel_rails cluster::start
    成功的話會跑在 port 3000 和 3001。沒成功的話可以讀 REDMINE_HOME/log/mongrel.3000.log 了解原因。 REDMINE_HOME 內所有檔案擁有者和群組記得設成 www-data。
  6. 安裝和啟動 apache 需要的 modules
    a2enmod proxy
    a2enmod proxy_http
    a2enmod proxy_balancer
    a2enmod rewrite
    
  7. 設定 apache conf, 我是寫在 /etc/apache2/conf.d/011_redmine.conf 裡:
    <Proxy *>
    Order deny,allow
    Allow from all
    </Proxy>
    
    <Proxy balancer://redmine_cluster>
    BalancerMember http://YOUR_IP:3000
    BalancerMember http://YOUR_IP:3001
    </Proxy>
    
    ProxyPreserveHost On
    
    <Location "/redmine">
        ProxyPass balancer://redmine_cluster
    
        RewriteEngine On
        # Redirect all non-static requests to cluster
        RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
        RewriteRule ^/(.*)$ balancer://redmine_cluster%{REQUEST_URI} [P,QSA,L]
    </Location>
    
    注意預設 apache proxy 是 deny all, 沒打開 apache error log 會出現 "client denied by server configuration ..."
  8. 執行
    /etc/init.d/apache2 reload

目前沒設 /etc/init.d/mongrel_cluster, 以後有機會再來研究。

2010年5月7日 星期五

CSS 覆寫屬性的規則

之前我都很蠢地在最上層用 <div id='content'> 包住所有內容, 然後在 css 裡寫 #content div { ... } 定基本共通的屬性。結果當 <div id='content'> 裡面需要用某個特別的 div 時, 怎麼設都無法取代之前設的屬性。我以為後寫的會覆蓋先寫的 (last match), 但看來又不全然如此。今天查了一下, 看到這篇說愈明確的 css selector 的優先權愈高。

於是改用 <div class='content'> 和 .content div { ... } 後就解決了。之後要在內部改變 div 屬性時, 萬一怎麼都搞不定, 至少用 id 可以蓋過去。難怪看到 jQuery UI 裡都用一堆 class, 沒事還是別用 id 設 css, 之後再來找時間研究一下較好的配套作法。


2010年5月6日 星期四

制作簡單的 API: 在 Django 中回傳 JSON

將 HttpResponse 的 mimetype 設為 json 即可, 程式如下:
# views.py
def myapi(request):
    data = {
       'name': 'Alice',
        ...
    }
    return HttpResponse(simplejson.dumps(data), mimetype='application/json')

設好 urls.py 後 (/myapi/), 就能用 jQuery 的 $.ajax 輕鬆地 call /myapi/ 取得 JSON ( callback function 的參數會是 JavaScript object, 不用再呼叫 JSON.parse )。為了減少出錯的機會, 我將 ajax 的參數 aysnc 設為 false。

回傳值是 JSON 有許多好處, 除方便用 AJAX 拿資料外, 也方便寫 unit test:
class MyApiTest(django.test.TestCase):
    data = { ... }
    response = self.client.post('/myapi/', data)
    self.assertEqual(200, response.status_code)
    expected = { ... }
    self.assertEqual(expected, simplejson.loads(response.content))

附帶一提, 有人寫了個 decorator 將一般的 view 轉成回傳 json, 目前沒這需求, 留著備忘。

2010年5月5日 星期三

django + mod_wsgi + virtualenv 注意事項

官網文件寫得滿清楚的, 這裡摘錄重點:
  • 為了避免用到系統裝的 package, 要將 virtualenv 載入的 package 放在 sys.path 前面, 這有幾種解法。
  • 第一種做法是在 apache config 裡寫: WSGIPythonHome /usr/local/pythonenv/BASELINE。BASELINE 是自己建的空 virtualenv。如此一來  mod_wsgi 就不會載入系統裝的 package, 自然沒這問題。
  • 但若 apache 同時用了 mod_python 和 mod_wsgi 的話, WSGIPythonHome 會失效 ( 我之前裝的 ReviewBoard 就是用 mod_python, 不知後來如何)。這裡有一點相關說明。
  • 在沒設 WSGIPythonHome 的情況下, 執行 mod_wsgi 時, sys.path 已載入系統裝的 packages 了。在這種情況下有兩種解法。
  • 若用了 mod_wsgi 2.4 之後的版本, mod_wsgi 有修改 site.addsitedir(), 讓它把 package path 加到 sys.path 的前面, 所以不用多做任何事, 就沒有這個問題。
  • 但是 Ubuntu 8.04 用的是 mod_wsgi 2.0, 所以得採用另一個作法, 也就是官網在 Application Environments 章節最後那段程式, 呼叫 site.addsitedir() 後自己重排 sys.path 內的順序。
  • 另外一點, 若只有一組 virtualenv, Application Environments 和 Process Environments 兩者做一個就行了。前者是在 WSGI  script (也就是 python script) 裡加 site-packages 到 sys.path 裡;後者是用 apache directive 指定 python path。
  • Process Environments 一次設好全部 WSGI application 的 virtualenv。但若有多個 WSGI application 需要不同的 virtualenv, 就得用 Application Environments 的方式各別設定。比方說 project A 用 Django-1.1, project B 用 Django-1.2, 那就得建兩個 virtualenv, 用 Application Environments 的方式各別設。
我最後是用 WSGIPythonHome + Application Environments + Daemon mode 的作法搞定。
附帶一提, 官網說一般情況 embedded mode 效能可能較好 (但官網也說應該差不了多少)。而我選 daemon mode 是因為這樣改程式後要重載入的話, touch WSGI script 即可, 不用 apache reload。

2010年5月2日 星期日

用 PGP 驗證下載的壓縮檔

剛才下載 Hadoop 的東西來試, 看到一些疑似驗證的檔案 KEYS 和 hadoop-0.20.2.tar.gz.asc。雖說不理它也可以繼續做事, 但看到神祕的檔案卻沒弄清楚, 感覺挺彆扭的。以前只試過比對 md5 checksum, 這回是第一次用 PGP。上網查了一下, 大部份文章都說它用在送信、運用 public-private key 來做加解密、以及一些信件相關軟體, 卻沒提到下載檔案的例子。後來靈機一動, 查 "tar.gz.asc" 查到這篇文章, 才找到使用的例子。

在 Ubuntu 下的操作過程:
  1. sudo aptitude install pgpgpg  # 安裝 gpg
  2. 下載 KEYS、hadoop-0.20.2.tar.gz.asc、hadoop-0.20.2.tar.gz
  3. gpg --import KEYS  # 載入壓 hadoop tarball 作者的 PGP public keys
  4. gpg --verify hadoop-0.20.2.tar.gz.asc  # 驗證 tarball 的 signature
試的結果, gpg --verify X.asc 後會拿 X 來計算看看 signature 是否一致。

2010年5月1日 星期六

Django URL 相對路徑的設法

看似很單純的事, 結果做起來有夠麻煩, 不知大家是怎麼做的? 網路上的範例幾乎都是將 project 放到根路徑, 即 http://.../。但若想在一台機器上放多個 project 的話, 就得額外處理。可能的做法有:
  • 註冊多個 domain name, 配合 apache 的 virtualhost 將不同 domain name 對到不同的 project。如此一來全部 project 都能用根路徑。
  • 將 project 放到子路徑: http://.../foo/, http://.../bar/。
我不想申請 domain name, 所以就選第二個方案。需要更改的東西有:
  • settings.py
  • urls.py
  • myapp/views.py
  • template/*.html
  • apache conf
做法如下。

settings.py

# 不同 project 設不同的 site_root, 像是 '/foo/' 或 '/bar/'
SITE_ROOT = '/'
MEDIA_URL = SITE_ROOT + 'media/'
ADMIN_MEDIA_PREFIX = SITE_ROOT + 'admin_media/'
# auth 要用到的 URL 設定
LOGIN_URL = SITE_ROOT + 'login/'
LOGIN_REDIRECT_URL = SITE_ROOT
在 views 裡記得用 RequestContext, 這樣 template 裡才能直接取用 {{ MEDIA_URL }}。

urls.py

# 路徑都改用 url 設名字, 之後才能在 template 中用 url tag 指回 view。
url(r'^$', views.index, name='index'), 
url(r'^login/$', 'django.contrib.auth.views.login', name='login'),
url(r'^logout/$', 'django.contrib.auth.views.logout_then_login', name='logout'),
...
# 順便提一下, 跑在 production server 上時會直接用 apache 傳靜態檔案, 
# 但開發時是用 Django 的 web server, 所以要要自己處理靜態檔案
if settings.DEBUG:
    # Define url routes for static files in development mode.
    urlpatterns += patterns('django.views.static',
                            (r'^media/(?P<path>.*)$',
                             'serve', {
                                 'document_root': settings.MEDIA_ROOT,
                                 'show_indexes': True}),)

myapp/views.py

一般情況直接 call views 中的 function 名稱, 但用 HttpResponseRedirect 時就得用 SITE_ROOT, 比方像這樣:
@login_required()
def some_page(request, project_id):
    if not request.user.is_staff:
        return HttpResponseRedirect(settings.SITE_ROOT)
    ...

template/*.html

用 url tag, 比方說:
<a href="{% url index %}">Homepage</a>

apache conf

我設在 /etc/apache2/conf.d/MY_DJANGO_CONF.conf。大致設法和 Django 用 mod_wsgi 差不多, 只是改一下路徑:
# Alias other operations to wsgi script.
WSGIScriptAlias /foo FILE_PATH_TO_WSGI_SCRIPT
WSGIScriptAlias /bar FILE_PATH_TO_WSGI_SCRIPT

<Location "/foo/media">
    SetHandler None
</Location>

<Location "/bar/media">
    SetHandler None
</Location>

Alias /foo/media FILE_PATH_TO_MEDIA
Alias /bar/media FILE_PATH_TO_MEDIA

# 若用 virtualenv 的話就指到 virtualenv 內的 site-packages
Alias /foo/admin_media /usr/lib/python2.5/site-packages/Django-1.1.1-py2.5.egg/django/contrib/admin/media
Alias /bar/admin_media /usr/lib/python2.5/site-packages/Django-1.1.1-py2.5.egg/django/contrib/admin/media

2010-05-06 更新

今天發現要在 JavaScript 內用 AJAX 連線, 於是得在 JavaScript 裡取得 settings.SITE_ROOT, 作法如下:
  • 在 settings.py 裡註冊一個 context processor:
    TEMPLATE_CONTEXT_PROCESSORS = (
        'django.core.context_processors.auth', # 頭四個是預設的, 覆寫 TEMPLATE_CONTEXT_PROCESSORS 時記得加進去
        'django.core.context_processors.debug',
        'django.core.context_processors.i18n',
        'django.core.context_processors.media',
        'mysite.myapp.views.add_constants',  # 自己用的
    )
  • 在 views.py 裡寫 add_constants:
    def add_constants(request):                                            return {
            'SITE_ROOT': settings.SITE_ROOT,
        }
    
    這兩步的目的是讓每個 view 都傳 SITE_ROOT 到 RequestContext 裡。
  • 在 template 裡置入 SITE_ROOT 的值, 寫到 base.html 裡:
    <script language="javascript" type="text/javascript">
        my_app.site_root = '{{ SITE_ROOT }}'; // my_app 是 global object, 作為我的 js code 的 root object
    </script>
    
    如此一來, 就能用 my_app.site_root 取得值。
真不是普通的麻煩啊。

寫個 filter 取出 model 欄位中的 verbose_name

查了一下 Django 好像沒內建這個功能, 要自己從 model 的 _meta 取出 field 的 verbose_name。就自己寫了個 filter:
# mysite/myapp/templatetags/myapp_extras.py
from django import template
from django.db.models.fields import FieldDoesNotExist

from mysite.myapp import models

register = template.Library()

@register.filter(name='verbose_name')
def verbose_name(value):
    model_name, field_name = value.split('.')
    model = getattr(models, model_name, None)
    if not model:
        return ''
    try:
        return model._meta.get_field(field_name).verbose_name
    except FieldDoesNotExist, e:
        return ''
在 template 裡的用法:
{% load myapp_extras %}
...
{% mymodel.myfield|verbose_name %}
...
filter 的寫法可以參見官網的《Custom template tags and filters》, 滿簡單的。

附帶一提, 要從字串拿出 model 時, 我楞了一下, 才發覺過去都沒想過「Python reflection API」。第一印象想到的是這種東西:
exec('model = %s' % model_name)
後來才想到不就是用 getattr 嗎..., 簡單到忘了它就是 reflection API。

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...