2015年7月19日 星期日

Python Django:Dinbendon(2)

  1. 建立餐廳列表
    要新增功能步驟一樣先設定urls.py
#.../Dinbendon/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.

#.../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>

最後則是view.py,加入一個新函數list_restaurants:

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

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

結果:
enter image description here

  1. 修改menu.html
    由於menu.html不再負責顯示餐廳的資訊,而且也只負責顯示一家餐廳的menu,所以必須要修改一下.
#.../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>
  1. 建立出由餐廳列表連結至某餐廳menu的功能
    這邊列出兩種方法實作
    方法一:
#.../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

#.../restaurants/views.py

from django.http import HttpResponse, 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會讓我們回到餐廳列表直到我們選定了餐廳為止。

方法二

#.../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 %}
# 以下略...

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

  1. 製作餐廳評價系統
    在這邊實際操作一下POST方法
#.../Dinbendon/urls.py
# 以上略...

from restaurants.views import menu, list_restaurants, comment

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

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

#.../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(也許我們需要通知他呢)。

#.../restaurants/models.py
from restaurants.models import Restaurant, Food, Comment
# 略...

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)

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

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

# 中略...


admin.site.register(Comment)

接下來是準備comment模版,這包含兩部分,我們會在頁面上方顯示評價,我們會在頁面下方顯示提交評價的表單

#.../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,他會依照格式顯示年月日, 另外使用了一個hiddentype<input>標籤,我們將利用檢查該表單元件的鍵值對是否有出現在request.POST中來判定表單是否被提交過。

#.../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模版來回應。

  1. 對提交的表單進行驗證
    能夠進行表單驗證的方式有很多種,javascript就提供了一些不錯的方法,不過那畢竟是在用戶端進行的驗證,我們必須保證來到伺服器端的資料也是正確的,也就是說,我們希望在後端也進行驗證。

可以透過新增一個變數error在views.py裡來判斷表單有無正確填寫,但是如果現在有超級多的表單欄位呢 => 檢查空白的運算式會超長的,而且表單重填的工作量會超大. 所以這邊決定練習另外一種方式來對表單進行驗證.

表單模型化
- 建立表單模型
在restaurants應用的目錄下新增一個forms.py的檔案,他負責處理該應用的表單。

#.../restaurants/forms.py
from django import forms

class CommentForm(forms.Form):
    user = forms.CharField(max_length=20)
    email = forms.EmailField(max_length=20, required=False)
    content = forms.CharField(max_length=200)

每個Form類型都繼承自forms.Form,在該class底下我們可以設定該表單所擁有的欄位,不同類型的欄位對應到forms中的不同型別,只是要記得各欄位現在來自forms庫。

每個欄位的要求限制可以用參數的方式描述,比如說最大長度max_length或是此欄為選填(非必填)required=False。變數設置的順序也很重要,會影響到預設輸出的順序,盡量得依照最後輸出到html上的順序來設置。

  • 操作表單模型
    表單模型輸出的方式不只一種,它也支援了<p><ul>的輸出形式:

$ python mangae.py shell
- Form.as_table() => 表格輸出
- Form.as_p() => 段落輸出
- Form.as_ul() => 列表輸出

  • 輸出表單
    這種html的輸出特性讓表單模型可以作為模版上的變量輸出
#.../restaurants/views.py
from restaurants.forms import CommentForm
...

def comment(request,id):
    if id:
        r = Restaurant.objects.get(id=id)
    else:
        return HttpResponseRedirect("/restaurants_list/")
    if 'ok' in request.POST:
        f = CommentForm(request.POST)
        if f.is_valid():
            user = f.cleaned_data['user']
            content = f.cleaned_data['content']
            email = f.cleaned_data['email']
            date_time = datetime.datetime.now()
            c = Comment(user=user, email=email, content=content, date_time=date_time, restaurant=r)
            c.save()
            f = CommentForm(initial={'content':'我沒意見'}) #設定初始值
    else:
        f = CommentForm(initial={'content':'我沒意見'})
    return render_to_response('comments.html',locals())

當表單確定被提交後,我們會利用request.POST這個類字典當做CommentForm的字典參數產生一個表單物件,再透過is_valid方法檢查表單的正確性,如果正確,我們產生一個評價並存入資料庫,並且重設變量f為未綁定表單(空表單)。若不正確,變量f的各欄位依然會有原先填入的值。當然,如果表單位被提交,使用者將可以看到一個全新的空表單(未綁定表單)。

所謂綁定 => 表單已輸入資料、與資料綁定
透過表單物件的is_bound屬性,我們便可以得知它是否被綁定(已填入資料)。一個已綁定的表單物件,便可以進行驗證(未綁定的表單是不能進行驗證的)。

接著我們撰寫它要使用的模版

#.../restaurants/templates/comments.html
...
        {% if f.errors %}
        <p style="color:red;">
            Please correct the error{{ f.errors|pluralize }} below.
        </p>
        {% endif %}

        <form action="" method="post">
            {{ f.as_table }}
            <input type="hidden" name="ok" value="yes">
            <input type="submit" value="給予評價">
        </form>
...

在最上方去檢查了表單f的errors屬性,若有錯誤則提示用戶要更正下列提到的錯誤。
enter image description here

  • 客製化的表單輸出
    可以根據自己的web app調整出最適當的表格。

更換表單元件
表單物件是預設以<input>作為html元件的,如果我們想要更動這項設定,可以在表單模型中以參數widget來達成,比如說我們的content欄位想要用<textarea>而不是用預設的<input>,我們可以更動表單模型如下:

mysite/restaurants/forms.py
from django import forms

class CommentForm(forms.Form):
    user = forms.CharField(max_length=20)
    email = forms.EmailField(max_length=20, required=False)
    content = forms.CharField(max_length=200, widget=forms.Textarea)

我們透過widget參數設定其值為forms.Textarea,這將使得該文字輸入使用<textarea>元件而非<input>元件。這個設計良好的分離了視圖邏輯(widget參數指定用何種html元件呈現)與驗證邏輯(使用了CharField來驗證文字輸入)。

自定驗證規則
雖然表單中的各種欄位已經提供了預設的驗證規則,但畢竟不能滿足每一個人的需要,既然已經看到驗證這個工作可以移到表單模型上了,這種驗證邏輯與業務邏輯的分離應該是我們需要維持的,更何況我們希望維持一個統一的錯誤警告模式,解決這個狀況的方法就是在表單模型中加入以clean_表單欄位名稱為名的驗證方法:

#.../restaurants/forms.py
# -*- coding: utf-8 -*-

from django import forms

class CommentForm(forms.Form):
    user = forms.CharField(max_length=20)
    email = forms.EmailField(max_length=20, required=False)
    content = forms.CharField(max_length=200, widget=forms.Textarea)

    def clean_content(self):
        content = self.cleaned_data['content']
        if len(content) < 5:
            raise forms.ValidationError('字數不足')
        return content

從表單物件中拿出content欄位的cleaned_data,這點不用覺得奇怪,我們已經經過了基本的驗證(CharField的驗證,包含欄位不能為空等等),所以這邊自然可以拿到cleaned_data,否則會在更早的地方便知道錯誤,便也不會進行本項檢查了。

接著我們將已經進行完基本檢查的乾淨content數據從cleaned_data中拿出來,並且用len計算字數,小於5字我們便引發一個ValidationError例外,而使用的字串參數將會成為表單欄位驗證錯誤的提示。如果字數足夠,我們會回傳content作為驗證後的表單值。

沒有留言:

張貼留言