高見龍

iOS app/Ruby/Rails Developer & Instructor, 喜愛非主流的新玩具 :)

Template

上一章我們學到了怎麼設定網址跟對應的action,但畫面的呈現不可能永遠只有一個Hello BookStore!這麼美好。很多時候程式設計師是需要跟美術設計師合作的,通常的流程是工程師會從設計手上拿到HTML + CSS的檔案,如果你曾經用PHP開發過網站,我猜大概你可能是把拿到的.html改成.php,然後開始HTML裡面塞PHP程式碼。Django的MTV架構也差不多,而且Django雖然是Python寫的,但不同的是你不可以在Template裡寫Python的程式碼,你在Template只能用Template提供的API,以及一些filter跟tag(不管是內建或是自己寫的)。

主要的目的就是希望大家不要把以前那種寫義大利程式碼(Spaghetti code)的不良習慣帶來Django。

回來看一下我們原來的book/views.py

1
2
3
4
5
6
7
from django.http import HttpResponse

def index(request):
  return HttpResponse("Hello BookStore!")

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

接著我們把原本的HttpResponse改寫成:

1
2
3
4
5
6
7
8
from django.http import HttpResponse
from django.shortcuts import render_to_response

def index(request):
  return render_to_response('book/index.html')

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

如果你有注意到shortcuts字樣,應該不難猜得出來因為這個功能太常用了,所以本來其實要寫一堆東西(例如要用loader把template抓出來,再把結果render出來),變成一行render_to_response就搞定,我很偷懶,所以平常幾乎都是用render_to_response就夠了。

你可能會好奇,這個book/index.html要放在哪裡? 先等等,我們先改個設定,請打開專案根目錄的settings.py,在TEMPLATE_DIRS裡加一些東西:

1
2
3
4
5
6
TEMPLATE_DIRS = (
    'templates',
    # 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.
)

設定這個的目的是要告訴Django要去哪邊取得template的檔案,這裡我設定了templates。接著,我們要來自己在專案根目錄底下建立一個templates的資料夾,然後結構像這樣:

image

原來的這行程式碼:

return render_to_response('book/index.html')

就會知道去哪邊找index.html這個檔案了。

這樣一來,你從設計那邊拿到的HTML檔案,就可以依照像index.html這樣的規則來擺放了。

Template API

前面提到,在Template裡不可以放Python的程式碼,只能放Template提到的API。什麼是Template API? 我直接從Django官網文件偷一小段程式碼來看:

Template API Demo (template_api_demo.py) download
1
2
3
4
5
6
7
8
9
{% if latest_poll_list %}
    <ul>
    {% for poll in latest_poll_list %}
        <li><a href="/polls/{{ poll.id }}/">{{ poll.question }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

你看到的這些程式碼,是可以寫在.html裡的,反正到時候Django會把.html當做Template讀出來之後再解析它,最後把內容render給使用者看。

if .. else ..來說,因為在HTML裡沒辦法對齊(就算對齊了萬一被設計師拿回去改過回來可能又不齊了),所以Template API要用一個結束標記,告訴Django說這個if已經在這裡結尾了。

在Template裡,如果看到{% .. %}的,表示它是一個邏輯運算或是語法,通常不會有輸出值;而看到{{ .. }}的話,就是會把裡面的值給吐出來餵在HTML裡的。

Template API很多,在繼續往下之後,請你先看一下https://docs.djangoproject.com/en/1.3/ref/templates/api/這份資料,知道一下Template API是怎麼一回事,又跟一般的Python程式碼有什麼不同?

大概看過之後,接著我們來看看怎麼樣把資料給丟到Template裡呈現出來。

網址設定

Model的部份看過之後,接著介紹Django很重要的一個單元,就是怎麼在Django裡設定或設計你網站內的網址。

以前你可以會看到像這樣的網址:

http://www.eddie.com.tw/book/index.php?id=100

網址後面接問號又接了一些參數,從網址大概猜得出來這應該是編號(id)第100號的書。這樣的做法不是不好,只是近年來有更好的做法,而且對SEO(Search Engine Optimization)有一些加分的效果:

http://www.eddie.com.tw/book/100

這該怎麼做呢? 在Django的網址設定是交給一個叫做urls.py的檔案在管理(其實不限定只有一個,也可以多個/模組化的設定),先看一下它原來的樣子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.conf.urls.defaults import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
  # Examples:
  # url(r'^$', 'bookstore.views.home', name='home'),
  # url(r'^bookstore/', include('bookstore.foo.urls')),

  # Uncomment the admin/doc line below to enable admin documentation:
  # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

  # Uncomment the next line to enable the admin:
  url(r'^admin/', include(admin.site.urls)),
)

urls.py的主要功能,就是設定"當遇到某個網址的request的時候,請找指定的App裡的View的action處理"。設定網址的對應,可能會需要學一些簡單的Regular Expression語法。

我們先來設定一下整個網站的首頁,讓它交給books/views.py裡的index方法處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.conf.urls.defaults import patterns, include, url

# Uncomment the next two lines to enable the admin:
from django.contrib import admin
admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    url(r'^$', 'bookstore.book.views.index'),
    url(r'^books/(?P<book_id>\d+)/$', 'bookstore.book.views.detail'),
    # url(r'^bookstore/', include('bookstore.foo.urls')),

    # Uncomment the admin/doc line below to enable admin documentation:
    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    url(r'^admin/', include(admin.site.urls)),
)

其中這行:

url(r'^$', 'bookstore.book.views.index'),

表示「當遇到網址後面沒接東西的時候(也就是大家俗稱的"首頁"),請找book/views.py裡面的index方法處理」,前面的bookstore是整個專案的名稱。設定完之後如果你直接連網址http://127.0.0.1:8000/的話,會看到這個畫面:

image

跳出了ViewDoesNotExist的錯誤訊息了。看到錯誤訊息不用擔心,通常錯誤訊息裡都會跟你說哪邊有錯。原來是我們剛剛把首頁指向之後,但並沒有在對應的地方加上index方法,所以請打開book/views.py,加上index方法:

1
2
3
4
5
6
7
from django.http import HttpResponse

def index(request):
  return HttpResponse("Hello BookStore!")

def detail(request, book_id):
  return HttpResponse("Book ID = %s" % book_id)

存檔後再回來看,你應該就可以看到這個畫面:

image

如果你這樣設定:

url(r'books\.php', 'bookstore.book.views.index'),

這表示當你連到http://127.0.0.1:8000/books.php的時候,會把request丟給book/views.pyindex方法處理。看起來好像是PHP,但其實不是。二、三年前我就曾經用Django做過幫Microsoft做過某個網路行銷活動案,客戶沒有規定要用什麼程式語言(只要會動就好),不過為了讓客戶覺得我是用ASP.NET開發的,我還特地在urls.py加上.aspx的附檔名。

是有點蠢,不過也挺有趣的不是嗎? :)

接下來,我們再來看看這行:

url(r'^books/(?P<book_id>\d+)/$', 'bookstore.book.views.detail'),

意思就是說當遇到http://127.0.0.1:8000/books/100的網址的時候,那個100會被當做book_id傳給book/views.pydetail方法當做參數。

views.py裡的方法,通常我們會稱它叫做action,每個action方法的第一個參數,一定都是http request,每個action的回傳值,一定要是一個HttpResponseviews.py在一般的MVC架構裡,大概就是Controller之類的角色。

如果你有注意到在urls.py裡的這行註解,長得跟我們寫的不太一樣:

url(r'^bookstore/', include('bookstore.foo.urls')),

這其實是另一種url的設定,雞蛋不一定要放在同一個籠子裡。當你的專案如果小的時候,全部放在專案根目錄的urls.py是沒問題的(中央集權),但隨著功能越加越多,你的網址對應可能會越來越多、越來越複雜,這時候你可以在App裡自己建一個urls.py,然後把所有跟這個App有關的url"委任"給它管理(地方自治),不過這部份的細節就不再多說明,還請再參考官網文件URL dispatcher

即使我們會設定網址了,會寫對應的action了,但還是少了什麼.. 如果設計師拿一個切好版的HTML + CSS給我,難到我要像這樣用HttpResponse整個把它印出來? 當然不是,接著我們來看看在Django的MTV架構裡的T,Template!

資料表的關連

我們在開發資料庫相關的網站的時候,通常會用到許多資料表,而且這些資料表之間彼此都有關連性,接著我們就來看看在Django的Model要怎麼實作。

我們現在再來新增一個App:

> python manage.py startapp book

book/models.py的內容長得像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import models
from author.models import Profile
from django.contrib import admin

class Book(models.Model):
  title        = models.CharField(max_length = 150)
  page         = models.IntegerField()
  price        = models.IntegerField()
  author       = models.ForeignKey(Profile)
  publish_date = models.DateField()

  class Meta(object):
    db_table = "book"

class BookAdmin(admin.ModelAdmin):
  pass

admin.site.register(Book, BookAdmin)

這裡用到了ForeignKey,對應到了我們之前建的Profile類別,表示這本書的作者資料是指向profile的某一筆資料,書對作者來說是一個一對一的關係,但是一個作者可能會寫多本書,作者對書來說就是多對一的關係(雖然現實上一本書可能有超過一個以上的作者,不過我們就把它單純化,讓生活暫時美好一下..)

別忘了把book這個App也設定到settings.pyINSTALLED_APP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'author',
    'book',
)

執行python manage.py sql book,看一下它轉換出來的SQL語法:

1
2
3
4
5
6
7
8
9
10
11
BEGIN;
CREATE TABLE "book" (
    "id" integer NOT NULL PRIMARY KEY,
    "title" varchar(150) NOT NULL,
    "page" integer NOT NULL,
    "price" integer NOT NULL,
    "author_id" integer NOT NULL REFERENCES "profile" ("id"),
    "publish_date" date NOT NULL
)
;
COMMIT;

執行python manage.py syncdb,把這個Book類別也轉成資料表。因為我們剛才也把Book類別也註冊到Admin模組了,所以我們到後台也可以看得到它。你可以在新增Book資料時候發現它的Author欄位在後台變成下拉選單了:

image

方便吧 :)

Model其實還有很多東西可以介紹的,不過因為我們的目的是為了快速看過一遍Django這個web framework大概是怎麼回事,所以細節還請各位參考官網文件(一定要看!!)。

在Django裡做Database Migration

在Django新增Model是很容易的,只要建立一個繼承自models.Model的類別就行了。但比較麻煩的問題是,如果在syncdb之後還需要調整欄位的話,即使在修改Model屬性再做一次syncdb,會發現Django對於已經sync過的Model會忽略掉。所以如果在syncdb之後要再新增/修改/刪除欄位的話,大致上有兩種做法:

  1. 砍掉重練,砍掉之後再重新syncdb。
  2. 手動修改資料庫的欄位以符合Model的屬性。

如果在開發初期,第一種做法還滿簡單的。但萬一已經是在線上運作的系統就不能用這種方式;第二種做法雖然可行,但每次都得手動改,其實也滿麻煩的。

這時候,如果可以有資料庫的Migration功能就很方便了。為什麼資料庫需要做Migration? 直接修改資料庫欄位有什麼不好? 一來手工修改麻煩,二來如果同時有多人一起開發的時候,你手動修改了資料庫欄位,可能會造成別人寫的程式出現錯誤。最大的好處是,有了DB Migration,資料庫也可以做版本控制了。

在Ruby on Rails有內建的DB Migration的機制,在Django並沒有,不過可以透過另外的模組:south,來完成這個功能。

安裝

如果你有apt-get可以用:

> apt-get install python-django-south

或是使用easy_install來安裝:

> easy_install South

或是到這裡,直接下載整個壓縮檔,解壓縮之後進到該目錄裡,執行:

> python setup.py install


請注意,使用apt-get或是easy_install可能都會需要root權限!


安裝完成之後,請把south加入到專案的INSTALLED_APPS裡:

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'author',
    'south',
)

再來請執行一次syncdb,Django會幫你產生一個資料表south_migrationhistory,用來紀錄Migration的歷史紀錄:

> python manage.py syncdb
Syncing...
Creating tables ...
Creating table south_migrationhistory
Installing custom SQL ...
Installing indexes ...
No fixtures found.

Synced:
 > django.contrib.auth
 > django.contrib.contenttypes
 > django.contrib.sessions
 > django.contrib.sites
 > django.contrib.messages
 > django.contrib.staticfiles
 > django.contrib.admin
 > author
 > south

Not synced (use migrations):

使用

如果要把之前建好的author這個app加進Migration裡:

> python manage.py convert_to_south author

然後你會發現在author資料夾裡面多了一個叫做migrations的目錄,裡面放了一個0001_initial.py

接下來,當你在Model做任何欄位修改之後,只要執行這行:

> python manage.py schemamigration author add_mobile_column --auto

它就會幫你找出你做了哪些欄位上的修改。如果你的欄位沒有指定預設值並且不允許NULL的話,過程中south會問你一些問題,請你指定預設值給它。順利的話,它應該會在migrations資料夾底下產生一個0002_add_mobile_column.py

但做到這裡並還沒有正式的修改到資料庫,接下來請執行python manage.py migrate

> python manage.py migrate
Running migrations for author:
 - Migrating forwards to 0002_add_mobile_column.
 > author:0002_add_mobile_column
 - Loading initial data for author.
No fixtures found.

這樣就會把剛剛做的異動正式寫入資料庫了。

回復

前面提到,資料庫有做Migration最大的好處就是隨時可以回到某一個版本,例如我們想要回到最一開始的版本的話:

> python manage.py migrate author 0001
 - Soft matched migration 0001 to 0001_initial.
Running migrations for author:
 - Migrating backwards to just after 0001_initial.
 < author:0003_change_table_name
 < author:0002_add_mobile_column

這樣就搞定了,跟手動修改資料欄位比較起來,方便許多。

更多關於Model的使用

在上一個章節的Shell操作過程中,如果你有注意到的話,你會發現一段長得像這樣的東西:

>>> Profile.objects.all()
[<Profile: Profile object>]

但如果你想讓這個object能夠一眼就看得出來它是什麼內容的話,只要在Model的Profile類別裡覆寫一些method,我們再次打開author/models.py,原來的程式碼長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
from django.db import models

# Create your models here.
class Profile(models.Model):
  name     = models.CharField(max_length = 50)
  age      = models.IntegerField()
  tel      = models.CharField(max_length = 30)
  address  = models.CharField(max_length = 100)
  email    = models.EmailField()

  class Meta(object):
    db_table = "profile"

我們來幫它加一個__unicode__的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.db import models

# Create your models here.
class Profile(models.Model):
  name     = models.CharField(max_length = 50)
  age      = models.IntegerField()
  tel      = models.CharField(max_length = 30)
  address  = models.CharField(max_length = 100)
  email    = models.EmailField()

  def __unicode__(self):
    return self.name

  class Meta(object):
    db_table = "profile"

然後我們再到shell裡操作的時候,就會發現秀出來的資料變得不一樣了:

>>> from author.models import Profile
>>> eddie = Profile(name = "eddie kao", age = 30, tel = "0928123123", address = "Taipei, Taiwan", email = "eddie@digik.com.tw")
>>> eddie
<Profile: eddie kao>

看到了嗎? 它由原本的object變成會秀出名字了。其實原理就是當你在Python用print方法把物件給印出來的時候,它會呼叫這個類別裡實作的__str____repr__以及__unicode__,其實這三個有一些些微的差異,細節可參考Python的文件說明,這裡我們直接覆寫Profile類別__unicode__方法,讓它回傳name欄位的值。這樣的修改,稍候你在後台看資料的時候,也會發現它會由原本的object,變成秀出你設定的name欄位。

事實上,你還可以定義更多的方法,把一些程式邏輯給包在Model裡,這樣一來,你在View(Controller)的地方就可以讓程式碼變得更簡潔,也更容易維護。例如我們來簡單寫個檢查該名作者是否已經成年(年齡 > 18):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import models

# Create your models here.
class Profile(models.Model):
  name     = models.CharField(max_length = 50)
  age      = models.IntegerField()
  tel      = models.CharField(max_length = 30)
  address  = models.CharField(max_length = 100)
  email    = models.EmailField()

  def is_adult(self):
    return self.age >= 18

  def __unicode__(self):
    return self.name

  class Meta(object):
    db_table = "profile"

再來進到shell裡看看:

> bookstore  python manage.py shell
Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from author.models import Profile
>>> author = Profile.objects.get(id = 1)
>>> author
<Profile: eddie>
>>> author.is_adult()
True

原本你可能需要在View(Controller)層寫個if .. else ..來判斷,現在可以把這個判斷包在Model層裡,一來程式碼容易除錯,而且因為都集中在Model的話,萬一今天假設我們的法律把成年人條件改成滿20歲,你只要修改一下Model的邏輯判斷,其它程式碼不用修改就可以下班了,這樣感覺不是很歡樂嗎?

那個超佛心的管理後台呢?

記得我們在前面有提到Django有送我們一個很好用的後台管理系統嗎? 但那時候登入之後空空的什麼都沒有,接下來我們要來讓這個Profile類別在後台也可以管理。要讓Model在Admin模組可以看得到,有兩種做法,第一種是在App裡面新增一個admin.py,內容長得像這樣:

1
2
3
4
from django.contrib import admin
from author.models import Profile

admin.site.register(Profile)

這段程式碼的大意就是我們要把Profile類別給"註冊"到Admin模組裡,讓它也感覺得到它的存在。登入後台後,你應該可以看到Profile類別出現了:

image

你可以試著玩看看它的一些管理功能,試試從Admin模組去新增/修改/刪除/更新一些資料。除了剛剛在App裡新增admin.py之外,另一種做法就是把這個"註冊"的動作寫在Model裡,原來的Profile類別看起來會像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from django.db import models
from django.contrib import admin

# Create your models here.
class Profile(models.Model):
  name     = models.CharField(max_length = 50)
  age      = models.IntegerField()
  tel      = models.CharField(max_length = 30)
  address  = models.CharField(max_length = 100)
  email    = models.EmailField()

  def is_adult(self):
    return self.age >= 18

  def __unicode__(self):
    return self.name

  class Meta(object):
    db_table = "profile"

class ProfileAdmin(admin.ModelAdmin):
  pass

admin.site.register(Profile, ProfileAdmin)

兩種做法都可以,我個人比較偏第二種,因為可以少寫一個檔案(懶!),程式碼只要在Model找就行了。

到目前為止,我們很單純的都只有一個表格,但現實生活不可能有這麼美好的事,所以我們再來新增一些資料表,並且讓這些資料表彼此有一些關連。