你將創建一些表單,讓用戶能夠添加主題和條目,以及編輯既有的條目。你還將學習Django如何防范對基於表單的網頁發起的常見攻擊,這讓你無需花太多時間考慮確保應用程序安全的問題。
然后,我們將實現一個用戶身份驗證系統。你將創建一個注冊頁面,供用戶創建賬戶,並讓有些頁面只能供已登錄的用戶訪問。接下來,我們將修改一些視圖函數, 使得用戶只能看到自己的數據。你將學習如何確保用戶數據的安全。
1、讓用戶能夠輸入數據
建立用於創建用戶賬戶的身份驗證系統之前,我們先來添加幾個頁面,讓用戶能夠輸入數據。我們將讓用戶能夠添加新主題、添加新條目以及編輯既有條目。
當前,只有超級用戶能夠通過管理網站輸入數據。我們不想讓用戶與管理網站交互,因此我們將使用Django的表單創建工具來創建讓用戶能夠輸入數據的頁面。
1.1 添加新主題
創建基於表單的頁面的方法幾乎與前面創建網頁一樣:定義一個URL,編寫一個視圖函數並編寫一個模板。一個主要差別是,需要導入包含表單的模塊forms.py。
(1)創建表單
讓用戶輸入並提交信息的頁面都是表單,哪怕它看起來不像表單。用戶輸入信息時,我們需要進行驗證,確認提供的信息是正確的數據類型,且不是惡意的信息,如中斷服務器的代碼。然后,我們再對這些有效信息進行處理,並將其保存到數據庫的合適地方。這些工作很多都是由Django自動完成的。 在Django中,創建表單的最簡單方式是使用ModelForm,它根據我們在python-Django實踐的模型中的信息自動創建表單。創建一個名為forms.py的文件,將其存儲到models.py所在的目錄
中,並在其中編寫你的第一個表單:
from django import forms from .models import Topic class TopicForm(forms.ModelForm): class Meta: model = Topic fields = ['text'] labels = {'text': ''}
我們首先導入了模塊forms 以及要使用的模型Topic 。我們定義了一個名為TopicForm 的類,它繼承了forms.ModelForm 。最簡單的ModelForm 版本只包含一個內嵌的Meta 類,它告訴Django根據哪個模型創建表單,以及在表單中包含哪些字段。我們根據模型Topic 創建一個表單,該表單只包含字段text (的代碼讓Django不要為字段text 生成標簽。
(2)URL模式new_topic
這個新網頁的URL應簡短而具有描述性,因此當用戶要添加新主題時,我們將切換到http://localhost:8000/new_topic/。下面是網頁new_topic 的URL模式,我們將其添加到 learning_logs/urls.py中:
"""定義learning_logs的URL模式""" from django.conf.urls import url from . import views app_name='learning_logs' urlpatterns = [ # 主頁 url(r'^$', views.index, name='index'), # 顯示所有的主題 url(r'^topics/$', views.topics, name='topics'), # 制定主題的詳細頁面 url(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'), # 用於添加新主題的網頁 url(r'^new_topic/$', views.new_topic, name='new_topic'), ]
(3)視圖函數new_topic()
修改views.py,函數new_topic() 需要處理兩種情形:剛進入new_topic 網頁(在這種情況下,它應顯示一個空表單);對提交的表單數據進行處理,並將用戶重定向到網頁topics :
from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from .models import Topic from .forms import TopicForm def index(request): """學習筆記的主頁""" return render(request, 'learning_logs/index.html') def topics(request): """顯示所有的主題""" topics = Topic.objects.order_by('date_added') context = {'topics': topics} return render(request, 'learning_logs/topics.html', context) def topic(request, topic_id): """顯示特定主題的詳細頁面""" topic = Topic.objects.get(id=topic_id) entries = topic.entry_set.order_by('-date_added') context = {'topic': topic, 'entries': entries} return render(request, 'learning_logs/topic.html', context) def new_topic(request): """添加新主題""" if request.method != 'POST': # 未提交數據:創建一個新表單 form = TopicForm() else: # POST提交的數據,對數據進行處理 form = TopicForm(request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(reverse('learning_logs:topics')) context = {'form': form} return render(request, 'learning_logs/new_topic.html', context)
我們導入了HttpResponseRedirect 類,用戶提交主題后我們將使用這個類將用戶重定向到網頁topics 。函數reverse() 根據指定的URL模型確定URL,這意味着Django 將在頁面被請求時生成URL。我們還導入了剛才創建的表單TopicForm 。
(4)GET請求和POST請求
創建Web應用程序時,將用到的兩種主要請求類型是GET請求和POST請求。對於只是從服務器讀取數據的頁面,使用GET請求;在用戶需要通過表單提交信息時,通常使用POST請求。
處理所有表單時,我們都將指定使用POST方法。還有一些其他類型的請求,但這個項目沒有使用。
函數new_topic() 將請求對象作為參數。用戶初次請求該網頁時,其瀏覽器將發送GET請求;
用戶填寫並提交表單時,其瀏覽器將發送POST請求。
根據請求的類型,我們可以確定用戶請求的是空表單(GET請求)還是要求對填寫好的表單進行處理(POST請求)。
❶處的測試確定請求方法是GET還是POST。如果請求方法不是POST,請求就可能是GET,因此我們需要返回一個空表單(即便請求是其他類型的,返回一個空表單也不會有任何 問題)。我們創建一個TopicForm 實例(見❷),將其存儲在變量form 中,再通過上下文字典將這個表單發送給模板context(見❼)。由於實例化TopicForm 時我們沒有指定任何 實參,Django將創建一個可供用戶填寫的空表單。
如果請求方法為POST,將執行else 代碼塊,對提交的表單數據進行處理。我們使用用戶輸入的數據(它們存儲在request.POST 中)創建一個TopicForm 實例(見❸), 這樣對象form 將包含用戶提交的信息。
要將提交的信息保存到數據庫,必須先通過檢查確定它們是有效的(見❹)。函數is_valid() 核實用戶填寫了所有必不可少的字段(表單字段默認都是必不可少的),且輸入 的數據與要求的字段類型一致(例如,字段text 少於200個字符,這是我們在第18章中的models.py中指定的)。這種自動驗證避免了我們去做大量的工作。如果所有字段都有 效,我們就可調用save() (見❺),將表單中的數據寫入數據庫。保存數據后,就可離開這個頁面了。我們使用reverse() 獲取頁面topics 的URL,並將其傳遞 給HttpResponseRedirect() (見❻),后者將用戶的瀏覽器重定向到頁面topics 。在頁面topics 中,用戶將在主題列表中看到他剛輸入的主題。
5. 模板new_topic
下面來創建新模板new_topic.html,用於顯示我們剛創建的表單:
{% extends "learning_logs/base.html" %} {% block content %} <p>Add a new topic:</p> <form action="{% url 'learning_logs:new_topic' %}" method='post'> {% csrf_token %} {{ form.as_p }} <button name="submit">add topic</button> </form> {% endblock content %}
這個模板繼承了base.html,因此其基本結構與項目“學習筆記”的其他頁面相同。
- 實參action 告訴服務器將提交的表單數據發送到哪里,這 里我們將它發回給視圖函數new_topic() 。實參method 讓瀏覽器以POST請求的方式提交數據。
- Django使用模板標簽{% csrf_token %} 來防止攻擊者利用表單來獲得對服務器未經授權的訪問(這種攻擊被稱為跨站請求偽造 )。
- 我們只需包含模板變量{{ form.as_p }},就可讓Django自動創建顯示表單所需的全部字段。修飾符as_p讓Django以段落格式渲 染所有表單元素,這是一種整潔地顯示表單的簡單方式。
- Django不會為表單創建提交按鈕,因此定義了一個這樣的按鈕。
6、鏈接到頁面new_topic
接下來,我們在頁面topics.html 中添加一個到頁面new_topic 的鏈:
{% extends "learning_logs/base.html" %} {% block content %} <p>Topics</p> <ul> {% for topic in topics %} <li> <a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a> </li> {% empty %} <li>No topics have been added yet.</li> {% endfor %} </ul> <a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a> {% endblock content %}
這個鏈接放在了既有主題列表的后面。下圖顯示了生成的表單。請使用這個表單來添加幾個新主題。

1.2 添加新條目
現在用戶可以添加新主題了,但他們還想添加新條目。我們將再次定義URL,編寫視圖函數和模板,並鏈接到添加新條目的網頁。但在此之前,我們需要在forms.py中再添加一個 類。
(1)用於添加新條目的表單
我們需要在forms.py創建一個與模型Entry 相關聯的表單,但這個表單的定制程度比TopicForm 要高些:
from django import forms from .models import Topic, Entry class TopicForm(forms.ModelForm): class Meta: model = Topic fields = ['text'] labels = {'text': ''} class EntryForm(forms.ModelForm): class Meta: model = Entry fields = ['text'] labels = {'text': ''} widgets = {'text': forms.Textarea(attrs={'cols': 80})}
- 我們首先修改了import 語句,使其除導入Topic 外,還導入Entry 。新類EntryForm 繼承了forms.ModelForm ,它包含的Meta 類指出了表單基於的模型以及要在表單 中包含哪些字段。這里也給字段'text' 指定了一個空標簽。
- 我們定義了屬性widgets 。小部件 (widget)是一個HTML表單元素,如單行文本框、多行文本區域或下拉列表。通過設置屬性widgets ,可覆蓋Django選擇的默認小 部件。通過讓Django使用forms.Textarea ,我們定制了字段'text' 的輸入小部件,將文本區域的寬度設置為80列,而不是默認的40列。這給用戶提供了足夠的空間,可以 編寫有意義的條目。
(2) URL模式new_entry
在用於添加新條目的頁面的URL模式中,需要包含實參topic_id ,因為條目必須與特定的主題相關聯。該URL模式如下,我們將它添加到了learning_logs/urls.py中:
"""定義learning_logs的URL模式""" from django.conf.urls import url from . import views app_name='learning_logs' urlpatterns = [ # 主頁 url(r'^$', views.index, name='index'), # 顯示所有的主題 url(r'^topics/$', views.topics, name='topics'), # 制定主題的詳細頁面 url(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'), # 用於添加新主題的網頁 url(r'^new_topic/$', views.new_topic, name='new_topic'), # 用於添加新條目的頁面 url(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'), ]
這個URL模式與形式為http://localhost:8000/new_entry/id / 的URL匹配,其中 id 是一個與主題ID匹配的數字。代碼(?P<topic_id>\d+) 捕獲一個數字值,並 將其存儲在變量topic_id 中。請求的URL與這個模式匹配時,Django將請求和主題ID發送給函數new_entry() 。
(3)視圖函數new_entry()
視圖函數new_entry() 與函數new_topic() 很像,在views.py中添加:
from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from .models import Topic from .forms import TopicForm,EntryForm def index(request): """學習筆記的主頁""" return render(request, 'learning_logs/index.html') def topics(request): """顯示所有的主題""" topics = Topic.objects.order_by('date_added') context = {'topics': topics} return render(request, 'learning_logs/topics.html', context) def topic(request, topic_id): """顯示特定主題的詳細頁面""" topic = Topic.objects.get(id=topic_id) entries = topic.entry_set.order_by('-date_added') context = {'topic': topic, 'entries': entries} return render(request, 'learning_logs/topic.html', context) def new_topic(request): """添加新主題""" if request.method != 'POST': # 未提交數據:創建一個新表單 form = TopicForm() else: # POST提交的數據,對數據進行處理 form = TopicForm(request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(reverse('learning_logs:topics')) context = {'form': form} return render(request, 'learning_logs/new_topic.html', context) def new_entry(request, topic_id): """在特定的主題中添加新條目""" topic = Topic.objects.get(id=topic_id) if request.method != 'POST': # 未提交數據,創建一個空表單 form = EntryForm() else: # POST提交的數據,對數據進行處理 form = EntryForm(data=request.POST) if form.is_valid(): new_entry = form.save(commit=False) new_entry.topic = topic new_entry.save() return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id])) context = {'topic': topic, 'form': form} return render(request, 'learning_logs/new_entry.html', context)
- 我們修改了import 語句,在其中包含了剛創建的EntryForm 。new_entry() 的定義包含形參topic_id ,用於存儲從URL中獲得的值。渲染頁面以及處理表單數據時,都 需要知道針對的是哪個主題,因此我們使用topic_id 來獲得正確的主題。
- 我們檢查請求方法是POST還是GET。如果是GET請求,將執行if 代碼塊:創建一個空的EntryForm 實例。如果請求方法為POST,我們就對數據進行處理: 創建一個EntryForm 實例,使用request 對象中的POST數據來填充它;再檢查表單是否有效,如果有效,就設置條目對象的屬性topic ,再將條目對象保存到數據 庫。
- 調用save() 時,我們傳遞了實參commit=False ,讓Django創建一個新的條目對象,並將其存儲到new_entry 中,但不將它保存到數據庫中。我們將new_entry 的屬性topic 設置為在這個函數開頭從數據庫中獲取的主題,然后調用save() ,且不指定任何實參。這將把條目保存到數據庫,並將其與正確的主題相關聯。
- 我們將用戶重定向到顯示相關主題的頁面。調用reverse() 時,需要提供兩個實參:要根據它來生成URL的URL模式的名稱;列表args ,其中包含要包含在URL中的 所有實參。在這里,列表args 只有一個元素——topic_id 。接下來,調用HttpResponseRedirect() 將用戶重定向到顯示新增條目所屬主題的頁面,用戶將在該頁面的 條目列表中看到新添加的條目。
(4)模板new_entry
模板new_entry.html 類似於模板new_topic.html :
{% extends "learning_logs/base.html" %} {% block content %} <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p> <p>Add a new entry:</p> <form action="{% url 'learning_logs:new_entry' topic.id %}" method='post'> {% csrf_token %} {{ form.as_p }} <button name='submit'>add entry</button> </form> {% endblock content %}
我們在頁面頂端顯示了主題,讓用戶知道他是在哪個主題中添加條目;該主題名也是一個鏈接,可用於返回到該主題的主頁面。 表單的實參action 包含URL中的topic_id 值,讓視圖函數能夠將新條目關聯到正確的主題。除此之外,這個模板與模板new_topic.html完全相同。
(5)鏈接到頁面new_entry
我們需要在topic.html顯示特定主題的頁面中添加到頁面new_entry 的鏈接:
{% extends 'learning_logs/base.html' %} {% block content %} <p>Topic: {{ topic }}</p> <p>Entries:</p> <p> <a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a> </p> <ul> {% for entry in entries %} <li> <p>{{ entry.date_added|date:'M d, Y H:i' }}</p> <p>{{ entry.text|linebreaks }}</p> </li> {% empty %} <li> There are no entries for this topic yet. </li> {% endfor %} </ul> {% endblock content %}
我們在顯示條目前添加鏈接,因為在這種頁面中,執行的最常見的操作是添加新條目。下圖顯示了頁面new_entry 。現在用戶可以添加新主題,還可以在每個主題中添加任 意數量的條目。請在一些既有主題中添加一些新條目,嘗試使用一下頁面new_entry 。
1.3 編輯條目
下面來創建一個頁面,讓用戶能夠編輯既有的條目。
(1)URL模式edit_entry
這個頁面的URL需要傳遞要編輯的條目的ID。修改后的learning_logs/urls.py如下:
"""定義learning_logs的URL模式""" from django.conf.urls import url from . import views app_name='learning_logs' urlpatterns = [ # 主頁 url(r'^$', views.index, name='index'), # 顯示所有的主題 url(r'^topics/$', views.topics, name='topics'), # 制定主題的詳細頁面 url(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'), # 用於添加新主題的網頁 url(r'^new_topic/$', views.new_topic, name='new_topic'), # 用於添加新條目的頁面 url(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'), # 用戶編輯條目的頁面 url(r'^edit_entry/(?P<entry_id>\d+)/$', views.edit_entry, name='edit_entry'), ]
在URL(如http://localhost:8000/edit_entry/1/)中傳遞的ID存儲在形參entry_id 中。這個URL模式將預期匹配的請求發送給視圖函數edit_entry() 。
(2)視圖函數edit_entry()
修改視圖views.py,頁面edit_entry 收到GET請求時,edit_entry() 將返回一個表單,讓用戶能夠對條目進行編輯。該頁面收到POST請求(條目文本經過修訂)時,它將修改后的文本保存到數據庫中:
from django.shortcuts import render from django.http import HttpResponseRedirect from django.urls import reverse from .models import Topic, Entry from .forms import TopicForm,EntryForm def index(request): """學習筆記的主頁""" return render(request, 'learning_logs/index.html') def topics(request): """顯示所有的主題""" topics = Topic.objects.order_by('date_added') context = {'topics': topics} return render(request, 'learning_logs/topics.html', context) def topic(request, topic_id): """顯示特定主題的詳細頁面""" topic = Topic.objects.get(id=topic_id) entries = topic.entry_set.order_by('-date_added') context = {'topic': topic, 'entries': entries} return render(request, 'learning_logs/topic.html', context) def new_topic(request): """添加新主題""" if request.method != 'POST': # 未提交數據:創建一個新表單 form = TopicForm() else: # POST提交的數據,對數據進行處理 form = TopicForm(request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(reverse('learning_logs:topics')) context = {'form': form} return render(request, 'learning_logs/new_topic.html', context) def new_entry(request, topic_id): """在特定的主題中添加新條目""" topic = Topic.objects.get(id=topic_id) if request.method != 'POST': # 未提交數據,創建一個空表單 form = EntryForm() else: # POST提交的數據,對數據進行處理 form = EntryForm(data=request.POST) if form.is_valid(): new_entry = form.save(commit=False) new_entry.topic = topic new_entry.save() return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic_id])) context = {'topic': topic, 'form': form} return render(request, 'learning_logs/new_entry.html', context) def edit_entry(request, entry_id): """編輯既有條目""" entry = Entry.objects.get(id=entry_id) topic = entry.topic if request.method != 'POST': # 初次請求,使用當前條目填充表單 form = EntryForm(instance=entry) else: # POST提交的數據,對數據進行處理 form = EntryForm(instance=entry, data=request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id])) context = {'entry': entry, 'topic': topic, 'form': form} return render(request, 'learning_logs/edit_entry.html', context)
我們首先需要導入模型Entry 。我們獲取用戶要修改的條目對象,以及與該條目相關聯的主題。在請求方法為GET時將執行的if 代碼塊中,我們使用實 參instance=entry 創建一個EntryForm 實例。這個實參讓Django創建一個表單,並使用既有條目對象中的信息填充它。用戶將看到既有的數據,並能夠編輯它們。
處理POST請求時,我們傳遞實參instance=entry 和data=request.POST ,讓Django根據既有條目對象創建一個表單實例,並根據request.POST 中的相關數 據對其進行修改。然后,我們檢查表單是否有效,如果有效,就調用save() ,且不指定任何實參。接下來,我們重定向到顯示條目所屬主題的頁面,用戶將 在其中看到其編輯的條目的新版本。
(3)模板edit_entry
下面是模板edit_entry.html,它與模板new_entry.html類似:
{% extends "learning_logs/base.html" %} {% block content %} <p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p> <p>Edit entry:</p> <form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'> {% csrf_token %} {{ form.as_p }} <button name="submit">save changes</button> </form> {% endblock content %}
實參action將表單發回給函數edit_entry()進行處理。在標簽{% url %}中,我們將條目ID作為一個實參,讓視圖對象能夠修改正確的條目對象。我們將提交按 鈕命名為save changes,以提醒用戶:單擊該按鈕將保存所做的編輯,而不是創建一個新條目。
(4) 鏈接到頁面edit_entry
在顯示特定主題的頁面中,需要給每個條目添加到頁面edit_entry 的鏈接,修改topic.html:
{% extends 'learning_logs/base.html' %} {% block content %} <p>Topic: {{ topic }}</p> <p>Entries:</p> <p> <a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a> </p> <ul> {% for entry in entries %} <li> <p>{{ entry.date_added|date:'M d, Y H:i' }}</p> <p>{{ entry.text|linebreaks }}</p> <p> <a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entry</a> </p> </li> {% empty %} <li> There are no entries for this topic yet. </li> {% endfor %} </ul> {% endblock content %}
我們將編輯鏈接放在每個條目的日期和文本后面。在循環中,我們使用模板標簽{% url %}根據URL模式edit_entry和當前條目的ID屬性(entry.id)來確定URL。鏈接 文本為"edit entry",它出現在頁面中每個條目的后面。下圖顯示了包含這些鏈接時,顯示特定主題的頁面是什么樣的。

參考:
1、python編程,從入門到實踐
