about 4 years ago

  • HttpRequest
    • URL訊息
    • Header訊息 - META
    • 數據提交訊息 - GET與POST
      • 存在request中的提交數據
      • 表單提交到來源action
      • 建立餐廳列表
      • 改善menu模版
      • 選擇餐廳並觀看menu
        • 方法一:使用表單
        • 方法二:使用連結與get查詢字符
      • 製作餐廳評價

與使用者互動,是動態網站裡面最令人激動的部份了,本篇要向大家展示Django在這方面的實力!包括了取得使用者的瀏覽資訊、如何取得使用者發出的GET或是POST請求,最後會介紹如何將表單也模型化!但筆者在本篇也會想要仔細的分析一下要完成一個功能的所有手段及其優劣,讀者們也可以多加思考並找出最適合自己web的解決之道。

HttpRequest

還記得我們的視圖函式嗎!記得我們說過,所有的視圖函式(有跟某URL對應的函式)都應該以HttpRequest物件作為第一個參數。這是因為HttpRequest物件(之後會以request代稱HttpRequest)含有關於本次web request及使用者的重要資訊,簡單來說,沒有request,我們對於使用者的情況和需求以及提供的資訊會一無所知。

URL訊息

我們來簡單看看request包含了哪些有用的URL訊息吧,讓我們打開menu.html模版:

mysite/restaurants/menu.html
# 以上略...

    <body>
        <p>您現在的位置在{{ path }}</p>  # 加入本行

        {% for r in restaurants %}
            <h2>{{ r.name }}</h2>
            {% if r.food_set.all %}
                <p>本餐廳共有{{ r.food_set.all|length }}道菜</p>
# 以下略...

我們看到在模版上多出了一個{{ path }}變量,我們必須在視圖函式中準備好該變量:

mysite/restaurants/views.py
# 以上略...

def menu(request):
    path = request.path  # 加入本行

    restaurants = Restaurant.objects.all()
    return render_to_response('menu.html',locals())

然後我們進menu頁面瞧瞧!我們會發現在首行出現了位置的資訊,沒錯,request就是知道這些!如果你需要提示使用者在哪個位置,就不需要將位置資訊寫死到模版中了,透過request.path我們可以知道除了網域名稱以外的請求路徑。為了省下各位的時間,我們節錄重要的request屬性和方法在下表,不再多做演示(我們把時間留給更重要的東西):

屬性方法 說明 example(屬性值或方法回傳值)
request.path 扣除網域名稱的請求路徑(開頭會有一個反斜線) /menu/
request.get_host() 主機名稱(網域名稱) 127.0.0.1 or www.restaurants.com
request.get_full_path() 請求路徑+查詢字符(可能是來自於get方法) /menu/?r_id=1
request.is_secure() 是否通過https協定來訪問 True

Header訊息 - META

request物件裡面還有一個記錄header訊息的字典,叫做META,無論是使用者瀏覽器的資訊或是客戶端的IP位址,都可以查詢的到。大家可以把它列印出來看看有哪些訊息。列印的方法很簡單,先設定url對應,再撰寫視圖函式,view function範例如下:

本例參考自Django Book
def meta(request):
    values = request.META.items()  # 將字典的鍵值對抽出成為一個清單

    values.sort()                  # 對清單進行排序

    html = []
    for k, v in values:
        html.append('<tr><td>{0}</td><td>{1}</td></tr>'.format(k, v))
    return HttpResponse('<table>{0}</table>'.format('\n'.join(html)))

小提醒,不論是HttpResponse或是render_to_response都要記得import喔!

數據提交訊息 - GET與POST

我們都知道,GET和POST是Http裡面最重要的兩個方法,這兩個方法負責提交使用者表單的數據,簡單來說,兩者在表現上的差別在:GET方法會透過在請求路徑後面添增查詢字符來提交數據,而POST方法是隱性地傳送數據的鍵值對,比如說我們在請求的時候,需要一個參數id,下表簡單表現了兩者在請求上的不同:

方法 範例URL
GET www.restaurants.com/restaurant/?id=1
POST www.restaurants.com/restaurant

這使得POST方法更適合拿來做資訊需隱密的請求,而GET方法則能漂亮的提供一個查詢相同頁面的URL(每次使用這個URL總是能獲得一個id=1的頁面,POST方法可能就不行了),另外我們也會覺得GET方法適合用來取得數據和對應的頁面,POST方法適合利用提交數據去更動資料庫,筆者非常歡迎各位讀者上網去閱讀更多有關於兩個方法的資料以及使用時機。

存在request中的提交數據

這兩個方法所提交的數據都會以鍵值對的方式儲存於request物件中,分別存於request.GETrequest.POST,分別都是一個類似於字典的物件。我們可以透過存取字典的方式來存取他們。接著我們來示範如何對使用者使用歡迎訊息,我們先打開urls.py加入一個歡迎頁面的對應,注意,當下這個頁面我們希望他是project層級的而不是app層級的,所以等等撰寫視圖函式時要寫在下層mysite中:

mysite/mysite/urls.py
# 以上略...

from views import welcome # 這個views指的是mysite/mysite/views.py

from restaurants.views import menu

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^menu/$', menu),
    url(r'^welcome/$', welcome),
)

然後我們準備一個視圖給要回應的頁面(一樣是project層級的templates):

mysite/templates/welcome.html
<html>
    <head>
        <title> Welcome </title>
    </head>
    <body>
        <form action="/welcome/" method="get">
            <label for="user_name">您的名字</label>
            <input id="user_name" type="text" name="user_name">
            <input type="submit" value="進入網站">
        </form>
    </body>
</html>

在表單中,我們選用了GET方法,這表示我們提交表單後,表單內容中的鍵值對會形成查詢字符付在請求頁面的後面。

另外一個點在於action的值,我們發現在這裡我們填入的不是一個html檔或是CGI命令搞,是一個URL pattern(其實本來就是XDD),這真是太令人激動了(什麼!!你還沒感覺,那可能要試著多體會了),這代表我們可以再次透過urls.py中的對應表來呼叫相應的view function來回應,這讓網頁的回應依然是動態的。其實反過來想,如果沒有這個機制,我們還真是束手無策。

綜合以上兩點,如果user_name欄位中的名字填的是dokelung,表單提交後,會發出一個請求,這個請求的路徑是:

http://網域名/welcome/?user_name=dokelung

user_name=dokelung這個鍵值配對會被存入request.GET中,用request.GET['user_name']可以存取到'dokelung'(字串型態)這個值。

最後是撰寫對應的view function:

mysite/mysite/views.py
# 以上略...

def welcome(request):
    if 'user_name' in request.GET:
        return HttpResponse('Welcome!~'+request.GET['user_name'])
    else:
        return render_to_response('welcome.html',locals())

我們先利用'user_name' in request.GET來測試表單是否有被提交,如果結果是True,表示表單被提交過了,使用者輸入名字過了,所以我們回應一個歡迎訊息,如果是False,我們則以welcome.html模版來回應一個輸入表單。

這邊對於網頁不熟的人可能會有點迷惑,其實是welcome這個function負責處理歡迎頁面的事情,但是呼叫他的狀態有兩種,一種是提交過表單了(在本例中是來自於formaction屬性指定導入),一種是未提交表單(可能來自超連結或使用者自行輸入的URL)。對於未提交的我們需要回應表單頁面,以提交的我們要回應提交後的訊息,而判斷的標準就在於檢查request.GET,這種狀況會非常常見,請大家要特別注意。

另外負責回應兩種提交狀態的頁面可能也會使用相同的模版,比如說要再透過相同表單輸入下一筆資料。

表單提交到來源action

最後,我們可以小小的在做一點改進,我們發現表單頁面的回應是由於pattern/welcome/驅動的action,而表單的提交動作也是交給由/welcome/所對應的action,當表單遇到這種來源與去向相同的時候,我們可以讓form標籤的action屬性為空,這不僅方便,也排除了寫死在代碼中的隱憂。如下範例:

mysite/templates/welcome.html
# 以上略...
    <body>
        <form aciton="" method="get">
            <label for="user_name">您的名字</label>
            <input id="user_name" type="text" name="user_name">
            <input type="submit" value="進入網站">
        </form>
    </body>
</html>

可是以上的範例都沒有跟資料庫有所互動,我們來來繼續增強我們的app吧。

為了能夠演示更複雜的例子,我們現在決定讓一個頁面只顯示一家餐廳的menu,而我們要創造另外一個頁面,上面會有餐廳資訊的列表,並且可以選擇一家餐廳來觀看他的menu,這個更動的工程對初學者來說有點大,我們一步一步來。

建立餐廳列表

首先我們還是依照舊方法,設定urls.py:

mysite/mysite/urls.py
# 以上略...

from views import welcome
from restaurants.views import menu, list_restaurants  # 多匯入list_restaurants


urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^menu/$', menu),
    url(r'^welcome/$', welcome),
    url(r'^restaurants_list/$', list_restaurants)          # 新增一個對應

)

接著我們準備一個可以顯示餐廳列表的模版restaurants_list.html,其實這跟menu.html沒有差太多:

mysite/restaurants/templates/retaurants_list.html
<!doctype html>
<html>
    <head>
        <title> Menu </title>
        <meta charset='utf-8'>
    </head>
    <body>
        <h2>餐廳列表</h2>
        <table>
            <tr>
                <th>店名</th>
                <th>電話</th>
                <th>地址</th>
            </tr>
            {% for r in restaurants %}
                <tr>
                    <td> {{ r.name }} </td>
                    <td> {{ r.phone_number }} </td>
                    <td> {{ r.address }} </td>
                </tr>
            {% endfor %}
        </table>
    </body>
</html>

最後撰寫視圖函式(根本與之前的menu函數一樣!),加入一個新函數list_restaurants:

mysite/restaurants/views.py
# 以上略...

def list_restaurants(request):
    restaurants = Restaurant.objects.all()
    return render_to_response('restaurants_list.html',locals())

我們可以試著運行server並且進入頁面/restaurants_list/,我們的餐廳列表完成囉。

改善menu模版

由於menu.html不再負責顯示餐廳的資訊,而且也只負責顯示一家餐廳的menu,所以我們必須要動手修改一下他:

mysite/restaurants/templates/menu.html
<!doctype html>
<html>
    <head>
        <title> Menu </title>
        <meta charset='utf-8'>
    </head>
    <body>
        <h2>{{ r.name }}的Menu</h2>
        {% if r.food_set.all %}
            <p>本餐廳共有{{ r.food_set.all|length }}道菜</p>
            <table>
                <tr>
                    <th>菜名</th>
                    <th>價格</th>
                    <th>註解</th>
                    <th>辣不辣</th>
                </tr>
            {% for food in r.food_set.all %}
                <tr>
                    <td> {{ food.name }} </td>
                    <td> {{ food.price }} </td>
                    <td> {{ food.comment }} </td>
                    <td> {% if food.is_spicy %} 辣 {% else %} 不辣 {% endif %} </td>
                </tr>
            {% endfor %}
            </table>
        {% else %}
            <p>本餐廳啥都沒賣</p>
        {% endif %}
    </body>
</html>

接著我們暫時變動一下menu函數來測試模版,在這裡我們故意寫死一個id=1餐廳的查詢。

mysite/restaurants/views.py
# 以上略...

def menu(request):
    r = Restaurant.objects.get(id=1)
    return render_to_response('menu.html',locals())
# 以下略...

選擇餐廳並觀看menu

接著我們想要建立出由餐廳列表連結至某餐廳menu的功能,能夠做到這件事的手段非常的多,值得我們細細探討。

方法一:使用表單

既然我們講到表單,便先用表單來實作他,首先我們為餐廳列表加入表單:

mysite/restaurants/templates/restaurants_list.html
# 以上略...
    <body>
        <h2>餐廳列表</h2>
        <form action="/menu/" method="get">
        <table>
            <tr>
                <th>選取</th>
                <th>店名</th>
                <th>電話</th>
                <th>地址</th>
            </tr>
            {% for r in restaurants %}
                <tr>
                    <td> <input type="radio" name="id" value="{{r.id}}"> </td>
                    <td> {{ r.name }} </td>
                    <td> {{ r.phone_number }} </td>
                    <td> {{ r.address }} </td>
                </tr>
            {% endfor %}
        </table>
        <input type="submit" value="觀看menu">
        </form>
    </body>
</html>

在本例中我們使用的是radiobuttons輸入欄位,這是一個用於單選的表單元件,我們設定他的name="id"value="{{r.id}}",這讓我們選出餐廳之後可以在request.GET中找到鍵值對:

request.GET['id'] => {{r.id}}
           ------    --------
          來自於name  來自於value

而且餐廳r旁邊的radio button剛好會送出對應的參數r.id,接著我們來處理一下視圖函數menu:

mysite/restaurants/views.py
from django.http import HttpResponse, HttpResponseRedirect    # 記得多匯入HttpResponseRedirect

from django.shortcuts import render_to_response
from restaurants.models import Restaurant, Food

def menu(request):
    if 'id' in request.GET:
        print(type(request.GET['id']))
        r = Restaurant.objects.get(id=request.GET['id'])
        return render_to_response('menu.html',locals())
    else:
        return HttpResponseRedirect("/restaurants_list/")

一樣,我們先檢查request.GET中有沒有id,如果有我們就利用模型管理器objects的get方法,來取得對應的餐廳,並且透過render_to_response在menu.html中填入該餐廳的資訊,如果沒有提交的數據,則會將頁面重導至pattern/restaurants_list/對應的視圖函式。也就是說,沒有選出任何一家餐廳的話,menu function會讓我們回到餐廳列表直到我們選定了餐廳為止。

我們這邊稍微來談一下重導,要使用重導必須由django.http中匯入HttpResponseRedirect類別,他吃一個pattern參數,如果return了這個物件,則會將頁面重導至pattern對應的action。當然這邊我們也可以試著用render_to_response來實作這個重導:

 return render_to_response('restaurants_list.html',locals())

但這個方式只是在重複貼上一樣的代碼而已,不如交給pattern去選擇action,並且可以得到該視圖函式完整的支援(比如說你的locals在這邊可能根本就不能準備restaurants_list.html模版要用的變量)。

方法二:使用連結與get查詢字符

如果不使用表單,還是能夠使用GET方法,就是直接使用查詢字符,首先我們來看另外一個版本的restaurants_list.html:

mysite/restaurants/templates/restaurants_list.html
# 以上略...
            {% for r in restaurants %}
                <tr>
                    <td> <a href="/menu/?id={{r.id}}"> menu </a> </td>     # 改成本行
                    <td> {{ r.name }} </td>
                    <td> {{ r.phone_number }} </td>
                    <td> {{ r.address }} </td>
                </tr>
            {% endfor %}
# 以下略...

若單純演示本例,則省略的部份照舊即可,但為了完成功能清楚的頁面,請大家把省略部分的所有表單元件(包含submit按鈕或<form>標籤等),之後對於方法三的範例code也請依樣操作。

<a>標籤內的href屬性就跟form元件的action屬性一樣,他的值可以是pattern,這邊我們多附上屬於GET方法的查詢字符,並且給定查詢字符?id={{r.id}},其效果便如同之前使用表單一樣,因為這個id鍵值對也會被傳入到request.GET中,所以我們的視圖函式menu並不用修改。

方法三:使用連結與URL參數

我們再度修改模版如下:

mysite/restaurants/templates/restaurants_list.html
# 以上略...
            {% for r in restaurants %}
                <tr>
                    <td> <a href="/menu/{{r.id}}/"> menu </a> </td>     # 改成本行
                    <td> {{ r.name }} </td>
                    <td> {{ r.phone_number }} </td>
                    <td> {{ r.address }} </td>
                </tr>
            {% endfor %}
# 以下略...

並且更動urls.py:

mysite/mysite/urls.py
# 以上略...

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^menu/(\d{1,5})/$', menu),    # 多增加一個pattern參數!

    url(r'^welcome/$', welcome),
    url(r'^restaurants_list/$', list_restaurants)
)

最後我們要更改我們的視圖函數:

mysite/restaurants/views.py
# 以上略...

def menu(request,id):
    if id:
        r = Restaurant.objects.get(id=id)
        return render_to_response('menu.html',locals())
    else:
        return HttpResponseRedirect("/restaurants_list/")

透過URL參數,我們也可以pick到想要的餐廳menu。其實方法三還有數種實作的手段,理由是url patterns的參數給法不只這一種,不過這不在本篇討論的範圍內,我們將在後面的筆記中做說明。

製作餐廳評價

剛剛的示例都以GET方法為主,我們便來實際操作一下POST方法吧!為了演示POST方法,我們要新增一個功能:對餐廳的評價系統。第一步我們先來寫寫對應表吧。

mysite/mysite/urls.py
# 以上略...

from restaurants.views import menu, list_restaurants, comment

urlpatterns = patterns('',
    url(r'^admin/', include(admin.site.urls)),
    url(r'^menu/(\d{1,5})/$', menu),
    url(r'^welcome/$', welcome),
    url(r'^restaurants_list/$', list_restaurants),
    url(r'^comment/(\d{1,5})/$', comment),          # 加入新的對應

)

這個對應表新增了一個關於comment pattern,我們就如同menu頁面一樣,在URL中提供一個餐廳id以顯示對應的餐廳評價!

下一步我們來修改一下模版,使得我們透過餐廳列表可以連結到指定的餐廳評價頁面:

mysite/restaurants/templates/restaurants_list.html
# 以上略...
        <table>
            <tr>
                <th>選取</th>
                <th>店名</th>
                <th>電話</th>
                <th>地址</th>
                <th>評價</th>  # 加入評價欄位
            </tr>
            {% for r in restaurants %}
                <tr>
                    <!-- <td> <input type="radio" name="id" value="{{r.id}}"> </td> -->
                    <td> <a href="/menu/{{r.id}}/"> menu </a> </td>
                    <td> {{ r.name }} </td>
                    <td> {{ r.phone_number }} </td>
                    <td> {{ r.address }} </td>
                    <td> <a href="/comment/{{r.id}}/"> comment </a> </td>  # 加入評價連結
                </tr>
            {% endfor %}
        </table>
# 以下略...

處理好前置作業後,我們要先來完成評價模型,需要注意的是,一個評價與餐廳是多對一的關係,我們需要設置外鍵,為了知道是哪個使用者留的評價,我們也需要記錄使用者名稱日期以及email(也許我們需要通知他呢)。

mysite/restaurants/models.py
# 以上略...

class Comment(models.Model):
    content = models.CharField(max_length=200)
    user = models.CharField(max_length=20)
    email = models.EmailField(max_length=20)
    date_time = models.DateTimeField()
    restaurant = models.ForeignKey(Restaurant)

然後建立資料表:

$ python manage.py validate
0 errors found
$ python manage.py syncdb

有需要的話我們也把Comment模型註冊上admin:

mysite/restaurants/admin.py
from django.contrib import admin
from restaurants.models import Restaurant, Food, Comment

# 中略...


admin.site.register(Comment)

下一步是準備comment模版,這包含兩部分,我們會在頁面上方顯示評價,我們會在頁面下方顯示提交評價的表單(我知道很醜,所以大家複製貼上就好):

mysite/restaurants/templates/comments.html
<!doctype html>
<html>
    <head>
        <title> Comments </title>
        <meta charset='utf-8'>
    </head>
    <body>
        <h2>{{ r.name }}的評價</h2>
        {% if r.comment_set.all %}
            <p>目前共有{{ r.comment_set.all|length }}條評價</p>
            <table>
                <tr>
                    <th>留言者</th>
                    <th>時間</th>
                    <th>評價</th>
                </tr>
            {% for c in r.comment_set.all %}
                <tr>
                    <td> {{ c.user }} </td>
                    <td> {{ c.date_time | date:"F j, Y" }} </td>
                    <td> {{ c.content }} </td>
                </tr>
            {% endfor %}
            </table>
        {% else %}
            <p>無評價</p>
        {% endif %}

        <br /><br />

        <form action="" method="post">
            <table>
                <tr>
                    <td> <label for="user">留言者:</label> </td>
                    <td> <input id="user" type="text" name="user"> </td>
                </tr>
                <tr>
                    <td> <label for="email">電子信箱:</label> </td>
                    <td> <input id="email" type="text" name="email"> </td>
                </tr>
                <tr>
                    <td> <label for="content">評價:</label> </td>
                    <td> 
                        <textarea id="content" rows="10" cols="48" name="content"></textarea>
                    </td>
                </tr>
            </table>
            <input type="hidden" name="ok" value="yes">
            <input type="submit" value="給予評價">
        </form>
    </body>
</html>

這邊有兩個地方需要說明,第一個是我們對date_time的顯示使用了過濾器date,他會依照格式顯示年月日(當然我們的date_time除了日期還有記錄時間,讀著們也可以試著將時間顯示出來)。第二個是我們使用了一個hiddentype的<input>標籤,我們將利用檢查該表單元件的鍵值對是否有出現在request.POST中來判定表單是否被提交過。

只差一步了,我們來完成我們的視圖函式!

mysite/restaurants/views.py
import datetime # 記得匯入datetime


# 中間略...


def comment(request,id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    if 'ok' in request.POST:
        user = request.POST['user']
        content = request.POST['content']
        email = request.POST['email']
        date_time = datetime.datetime.now()     # 擷取現在時間

        Comment.objects.create(user=user, email=email, content=content, date_time=date_time, restaurant=r)
    return render_to_response('comments.html',locals())

太棒了,恭喜大家完成了困難重重的更動與新增,在comment函式中,我們先檢查了id參數有沒有拿到,如果沒有就重導回餐廳列表。再來檢查表單有沒有被提交過(也就是檢查是不是第一次進來本頁面),有被提交過,我們便利用request.POST擷取表單個欄位內容並且利用Comment模型產生一個新物件(一筆新資料),最後我們一樣呼叫comments.html模版來回應。

好了!大功告成了吧!運行server觀賞頁面!不錯,歷來評價都有被我們顯示(讀者可以先前往admin偷塞幾筆comments給餐廳),接著我們試著填入表單並提交^_^,然後期待好消息,等等!!!失敗!!!!失敗!!!!怎麼可能!出現了錯誤,到底是怎麼一回事?

Forbidden(403):
CSRF verification failed. Request aborted.
Reason given for failure:
    CSRF token missing or incorrect.

這是因為python幫我們啟動了CSRF攻擊的防護,為了避免複雜,我們先將此功能關閉,打開settings.py,將CSRF的中間件設定取消:

mysite/mysite/settings.py
# 以上略...

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',

    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
# 以下略...

那我們的POST表單就沒問題囉。

同樣地,為了避免篇幅過長,我們在下一章會對表單做更深入的研究。

← Django筆記(6) - 後台管理系統Admin Django筆記(8) - 表單的驗證與模型化 →
 
comments powered by Disqus