- 建立餐廳列表
要新增功能步驟一樣先設定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())
結果:
- 修改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>
- 建立出由餐廳列表連結至某餐廳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>
使用的是radio
buttons輸入欄位,這是一個用於單選的表單元件,我們設定他的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並不用修改。
- 製作餐廳評價系統
在這邊實際操作一下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模版來回應。
- 對提交的表單進行驗證
能夠進行表單驗證的方式有很多種,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屬性,若有錯誤則提示用戶要更正下列提到的錯誤。
- 客製化的表單輸出
可以根據自己的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作為驗證後的表單值。
沒有留言:
張貼留言