電腦做什麼事 第十八章 利用樣版系統編排

上一章中建立了留言板的應用程式,接著進入後台進行管理,可是還沒有提及如何做出供他人瀏覽的網頁。我們的資料越來越多,該如何做出網頁呢?



第十六章我們做網頁的方法是直接把HTML寫進字串中,然後利用網址改變以及抓取電腦的現在時間,直接以變數提供給HTML的字串,最後作為HttpRespons()的參數,使之輸出到瀏覽器中。


這是利用Django做網頁的方法之一,當要放進網頁的內容越來越多,同時也希望進行更多的版面配置,使網頁呈現出美觀的外在,把HTML寫進字串的方法就顯得冗贅繁複。其實,這並不是Django做網頁唯一途徑,普遍的作法是利用樣版系統編排網頁的版面。


樣版系統結合HTML語法,輔以標籤過濾器,結合程式語言的部份功能,在Django中作為描述網頁編排的標記語言。這使Django相較其他環繞MVC原則的網頁框架有了一個明顯的特色,也就是Django除了遵守MVC原則外,另具備MTV的概念。


MTV的概念




MTV為Model-Template-View的頭字母縮寫詞,M與MVC原則中的M的意義一樣,V稍有差異,MTV的V為控制網頁顯示的方法。T為Template的第一個字母,中文意思就是樣版,樣版系統正是我們這一章的主題。


MTV可以直接與專案內的檔案連起來思考,M如同留言板應用程式中的models.py,V為views.py。至於T,因為一個樣版就是單一個檔案,專案中的樣版數沒有限制,這是說網頁的顯示依需要可以套用不同的樣版,實際上我們會建立一個名為「template」的資料夾放置樣版的檔案。


為什麼前面要介紹MVC,這邊要介紹MTV呢?兩者的內涵似乎重疊了許多?因為MVC是一般所倡導的概念,然而就Django而言,MVC的V是MTV中的T,MVC的C則是MTV中的V,兩者的概念說到底是一樣的。


原因不外由於MVC是很常用的詞,而MTV為針對解釋Django之情形所用的詞,MVC在Django之中容易誤解,尤其在Controller的部份,廣義的說整個Django都算是屬於Controller的部份,然而MTV就不會了,因為MTV各有專屬的檔案或資料夾。


我們製作樣版檔案會用到Django的樣版語言,這當中也需要一些HTML,這似乎是要進入另一個與Python完全不同的主體嗎?不是的,樣版系統作為網頁顯示的元件,其實這也是基於Django的設計哲學,「寬鬆的結合」,MTV每個元件各自分開獨立運作。


接著繼續下去我們會發現樣版語言跟Python頗為相似,不過還是得留心不同的地方。


now.html及page.html




我們延續上一章的demo專案,在demo資料夾內建立一個template資料夾。
建立template資料夾


然後我們先以第十六章「現在時間是 %s。」及「您來到了第 %s 頁;」的例子,簡介樣版系統的語法。樣版的檔案實際上就是HTML檔案,我們先在template資料夾中建立now.html,作為顯示現在時間的樣版,裡頭放入以下的內容。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

<html>
<head>
<title>顯示現在時間</title>
</head>

<body>
<h1>現在時間是 {{ current_time }} 。</h1>

你好啊!
右邊會隨機出現一個0到9之間的整數: {{ number|random }}
</body>
</html>


其中,兩個大括弧中間包著的current_time,
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{{ current_time }}


這在樣版中表示current_time為一個變數。而
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{{ number|random }}


number也是變數,其後的「|」為管線命令符號,經過random參數的隨機透析,使number變數中的數值以過濾後的方式呈現,這就是上述的過濾器。稍後,我們會抓取電腦現在時間儲存到current_time內,另外會指定一組數字給number。


另外在template資料夾中建立page.html,作為顯示頁數的樣版,內容如下。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

<html>
<head>
<title>顯示頁數及現在時間</title>
</head>

<body>
<h1>現在時間是 {{ current_time }} 。</h1>
{% ifequal offset "99" %}
<p>這是最後一頁囉!</p>
{% else %}
<p>您來到了第 {{ offset }} 頁。</p>
{% endifequal %}
</body>
</html>


變數是用兩個大括弧圍起來,而這裡
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{% ifequal offset "99" %}



<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{% endifequal %}


之間被稱為區塊,大括弧接百分比符號中間的{% ifequal %}則是標籤,標籤類似Python中的關鍵字,用於指揮程式進行的過程。這裡的{% ifequal %}後面接兩個參數,第一個為變數,第二個為數值,判斷其後所接的變數是否與數值相等,若是相等則在網頁中顯示區塊內的
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

<p>這是最後一頁囉!</p>


若是不相等則在網頁中顯示「else」標籤中的
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

<p>您來到了第 {{ offset }} 頁。</p>


記不記得在第十六章,這部份是寫在view.py的page_counter()函數之中,因為那時候網頁顯示的工作一併交給了view.py的page_counter()函數,現在的分開處理,正是Django設計哲學中「寬鬆的結合」最佳的例證。


有一點需要留意的,樣版語言的標籤與HTML類似,兩者都是成對出現的,如上例{% ifequal %}的標籤,標籤有效範圍終止的地方是用{% endifequal %}標籤,這也就代表了區塊的結束。


樣版目錄的設定




我們還需要做幾個設定調整才能夠讓網頁顯示,首先開啟setting.py,找到如下的部份。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/ 

TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or \
    "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
)


這些註解化的文字大略是說需要用字串的格式,將樣版目錄的路徑放置在這裡。我們簡單一點,將這部份的程式碼更改如下。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/ 

TEMPLATE_DIRS = (
    '.',
)


我們所加入的「'.'」,好比是將尋找樣版資料夾的根目錄設定為與專案相同。接著將guestbook資料夾中的views.py更改如下。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/ 

from django.shortcuts import render_to_response
import datetime

def current_datetime(request):
    cdt = datetime.datetime.now()
    now_template = r"template\now.html"
    now_context = {'current_time':cdt, 'number':range(10)}
    return render_to_response(now_template, now_context)

def page_counter(request, offset):
    cdt = datetime.datetime.now()
    page_template = r"template\page.html"
    page_context = {'current_time':cdt, 'offset':offset}
    return render_to_response(page_template, page_context)


這裡比較特別的是從django的shortcuts模組引入render_to_response()函數,需要兩個參數,第一個參數為指定樣版的路徑,儲存在字串之中,第二個參數則是樣版中變數與變數所儲存的數值,利用配對型態的字典來儲存,key為變數名稱,value則為變數內容,要留意需要用字串型態儲存用作變數名稱的key。


render_to_response()函數是一種捷徑函數,因為利用樣版顯示網頁,實際上需要先把樣版檔案傳換成Template物件,而變數名稱及內容則要轉換為Context物件,捷徑函數便利我們省卻了許多麻煩。


最後還須修改一下urls.py,我們替urlpatterns加入兩行的網址設定。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/ 

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^time/$', 'demo.guestbook.views.current_datetime'),
    (r'^time/(\d{1,2})/$', 'demo.guestbook.views.page_counter'),
)


現在可以來看看結果了,別忘記要啟動伺服器,然後連結到http://127.0.0.1:8000/time/。
顯示現在時間的頁面


再連結到其中的一頁看看吧!
第33頁


其他的標籤及過濾器




樣版系統除了支援HTML的註解標籤外,本身也有用作註解的標籤。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{# 請將註解放在這裡 #}


大括弧加井字號,這就是Django樣版語言所用的註解。除了註解之外,Python中用於條件判斷的if陳述,樣版語言也有相對應的標籤。


and、or、not也都支援可用,供比較測試多個變數。別忘了{% if %}標籤要以{% endif %}結尾。當然我們已經見過的{% else %}標籤也有支援,然而elif陳述沒有支援,所以當選擇判斷的條件多於兩個時,就要用巢狀的方式。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{% if a and b %}
變數a與b都為真的話,就會出現這些內容………
{% else %}
{% if c %}
變數a與b都不為真,但是c為真,就會出現這些內容………
{% else %}
變數a、b、c都不為真,就會出現這些內容………
{% endif %}
{% endif %}


就跟HTML標籤同理,任一個「if」標籤都要以一個「endif」結尾,不然Django會不知道到哪裡結束「if」,從而導致發生錯誤。


另外也有{% for %}標籤。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{# 這會依序顯示data中的資料… #}
{% for i in data %}
{{ i }}<BR />
{% endfor %}


這會一列一列的在網頁中顯示變數data所儲存的個別數值。


過濾器是針對變數運用的,我們多舉幾個例子說明,譬如變數one中儲存整數1
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

1+1={{ one|add:”1” }}


add會替變數加上其後的參數,於是顯示的結果會是「1+1=2」。又如果變數name存放英文姓名
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{{ name|capfirst }}


不論name中字母的大小寫,capfirst都會將第一個字母設定為大寫。我們上面的變數current_time,也可以利用過濾器提出個別的部份
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{{ current_time|date:”M” }}


date用大寫的「M」作為參數,網頁上只會顯示月份。當然,過濾器還有很多,我們再舉一個例子,如變數pi中所儲存的是圓周率3.14159……………..
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{{ pi|floatformat:3 }}


floatformat會調整pi在小數點後顯示的位數,這裡給了「3」作為參數,就是會在網頁中顯示圓周率為「3.141」。


Django樣版系統的標籤及過濾器還有很多,可以參考Built-in template tags and filters。


樣版的繼承




不論在now.html或是page.html中,我們都重複寫了<html></html>、<head></head>>及<body></body>等HTML標籤,另外也重複寫了「<h1>現在時間是 {{ current_time }} 。</h1>>」這一行。雖然目前例子很簡單,但是當網頁的內容頁數與日俱增,而每一頁又有許多相同的元件時,一再的重複,就顯得冗長累贅。


Python語言的繼承,讓某一型態可以輕易的獲取原先定義型態的屬性與方法,進而覆寫或是增加新的屬性。Djnago的樣版語言沿襲Python語言繼承的優點,我們可以把網頁的共同元件寫進一個基礎樣版中,不同類型的網頁可以繼承基礎樣版的內容。如下我們以base.html為基礎樣版。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

<html>
<head>
<title>{% block title %} 基礎樣版 {% endblock %}</title>
</head>

<body>
<h1>現在時間是 {{ current_time }} 。</h1>

{% block content %}{% endblock %}
</body>
</html>


標籤「block」通常會出現在某特定區域,如<title></title>這對HTML標籤之內,其後也需要接一個專屬的名稱,作為識別位置之用,然後在被繼承的樣版中,相同的「block」標籤名稱即可覆寫基礎樣版的內容。


我們另外寫now2.html與page2.html來繼承base.html,now2.html如下。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{% extends "template/base.html" %}

{% block title %}
顯示現在時間 
{% endblock %}

{% block content %}
你好啊!
右邊會隨機出現一個0到9之間的整數: {{ number|random }}
{% endblock %}


凡是需要繼承基礎樣版的樣版,當案開頭都要用標籤「extends」,其後接一個字串內含基礎樣版的路徑,然後將所欲覆寫的標籤內容補上,如此一來繼承工作就可順利進行。


省掉了重複撰寫的東西,是否單純了許多呢?page2.html如下。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{% extends "template/base.html" %}

{% block title %}
顯示現在時間及頁數 {{ offset }} 
{% endblock %}

{% block content %}
{% ifequal offset "99" %}
<p>這是最後一頁囉!</p>
{% else %}
<p>您來到了第 {{ offset }} 頁。</p>
{% endifequal %}
{% endblock %}


如須看網頁的結果,我們還須回到views.py修改樣版的路徑字串,也就是now_template與page_template兩個區域變數。這會跟稍早的結果一樣,請自行嘗試看看,別忘了要在伺服器啟動狀態下連結網頁。


留言板的索引頁 - index.html




我們接下來利用樣版系統做出上一章留言板的網頁瀏覽介面,同樣繼承自base.html,這樣網頁都會出現「現在時間」。總共需要兩個樣版,一個是所有留言的目錄,另一個是個別留言的瀏覽頁。


關於所有留言目錄的樣版index.html,如下。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{% extends "template/base.html" %}

{% block title %}
簡單的留言板 
{% endblock %}

{% block content %}
{% if entry_list %}
{% for entry in entry_list %}
<p><a href="/{{ entry.id }}">{{ entry.title }}</a></p>
{% endfor %}
{% else %} 
<p>尚無留言 = _ =</p>
{% endif %}
{% endblock %}


網頁標題設為「簡單的留言板」,然後在{% block content %}裡頭,我們用了{% if %}標籤,檢查是否有留言,若是沒有,網頁顯示「尚無留言」,若是有,便利用{% for %}標籤依留言的id屬性,將留言一個標題以一個段落顯示到網頁上。


id屬性繼承自Model物件,由留言的順序從1開始給予整數的序數,這也作為閱覽單獨留言的網址,這個網址「href="/{{ entry.id }}"」,如第一個留言是指http://127.0.0.1:8000/1/的位置。


接著在views.py加入index()函數,如下。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/

def index(request):
    cdt = datetime.datetime.now()
    index_template = r'template/index.html'
    index_context = {'current_time':cdt, 'entry_list': Entry.objects.all()}
    return render_to_response(index_template, index_context)


Entry.objects.all()會取得所有已經建立的Entry物件,當然,我們也要先引入Entry物件。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/

from demo.guestbook.models import Entry


我們預備把http://127.0.0.1:8000/的位置給索引頁,因此urls.py的urlpatterns修改如下。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/

urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^time/$', 'demo.guestbook.views.current_datetime'),
    (r'^time/(\d{1,2})/$', 'demo.guestbook.views.page_counter'),
    (r'^$', 'demo.guestbook.views.index'),
)


我們連結到http://127.0.0.1:8000/看看吧!
首頁 - 留言板的目錄


瀏覽個別留言 - entry.html




個別留言的樣版entry.html如下。
<!--《電腦做什麼事》的範例程式碼 
http://pydoing.blogspot.com/ -->

{% extends "template/base.html" %}

{% block title %}
{{ entry.title }} 
{% endblock %}

{% block content %}
<h1>留言標題: {{ entry.title }}</h1>
<h1>留 言 者: {{ entry.name }}</h1>
<h1>網    址: {{ entry.url}}</h1>
<p>{{ entry.text }}</p>
<p><a href="/">回到目錄</a></p>
{% endblock %}


網頁標題亦即Entry物件的title屬性,底下依序用<h1>標籤顯示留言標題、留言者、網址,留言本文與的「回到目錄」連結則是用<p>段落標籤,「回到目錄」也就是回到索引頁,所顯示的四個項目分別是Entry物件的四項屬性。


我們在views.py加入entry()函數處理個別留言的網頁顯示。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/

def entry(request, entry_id):
    cdt = datetime.datetime.now()
    message = get_object_or_404(Entry, pk=entry_id)
    entry_template = r'template/entry.html'
    entry_context = {'current_time':cdt, 'entry': message}
    return render_to_response(entry_template, entry_context)


這裡用了另一個捷徑函數get_object_or_404(),其能夠取得物件所有的屬性,所以要多引入get_object_or_404的名稱修改如下。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/

from django.shortcuts import render_to_response, get_object_or_404


連結網址設定urls.py的urlpatterns加入這一行。
#《電腦做什麼事》的範例程式碼 
#http://pydoing.blogspot.com/

(r'^(?P<entry_id>\d+)/$', 'demo.guestbook.views.entry'),


注意,「(?P<entry_id>\d+)」仍是正規表示法,這可以把連結網址設定為id屬性值。


我們來看看結果吧!先點擊第二筆留言「Django真好玩」。
第二筆留言


結果如上,我們再點擊「回到目錄」,然後回去看第一筆留言。
第一筆留言


沒錯,並無出現網址,網頁同時也正常顯示。


※ 本文同時登載於 OSSF 網站的下載中心 - PDF ※ 回 - 目錄