《Python編程:從入門到實踐》筆記。
本篇記錄如何建立用戶註冊系統,如何實現用戶輸入本身的數據。
在本篇中,咱們將:html
先添加幾個頁面,讓用戶可以添加新主題,新條目以及編輯條目。python
和以前建立網頁的步驟同樣:定義URL,編寫視圖函數,編寫模板。主要區別是,這裏須要一個包含表單的模塊forms.py
shell
用戶輸入信息時,須要進行驗證,確保提交的信息是正確的數據類型,且不是惡意信息,如中斷服務器的代碼。而後再處理信息,並保存到數據庫中。固然,這些工做不少都由Django自動完成。數據庫
在models.py
所在的目錄中新建forms.py
模塊。建立表單的最簡單方法是繼承Django的ModelForm
類:django
from django import forms from .models import Topic class TopicForm(forms.ModelForm): class Meta: model = Topic fields = ["text"] labels = {"text": ""}
最簡單的ModelForm
版本只包含一個內嵌的Meta
類,它告訴Django根據哪一個模型建立表單,以及在表單中包含哪些字段。第6行,咱們根據Topic
建立一個表單,該表單只包含字段text
(第7行),並不爲該字段生成標籤(第8行)。編程
當用戶要添加新主題時,將切換到http://localhost:8000/new_topic/ 。在learning_logs/urls.py
中添加以下代碼:瀏覽器
urlpatterns = [ -- snip -- # 用於添加新主題的網站 path("new_topic/", views.new_topic, name="new_topic"), ]
該函數須要處理兩種情形:①剛進入new_topic
網頁,顯示一個空表單;②對提交的表單數據進行處理,並將用戶重定向到網頁topics
。修改views.py
文件:bash
from django.http import HttpResponseRedirect from django.urls import reverse from .forms import TopicForm def new_topic(request): """添加新主題""" if request.method != "POST": # 爲提價數據:建立一個新表單 form = TopicForm() else: # POST提交的數據,對數據進行處理 form = TopicForm(request.POST) if form.is_valid(): form.save() # 該類將用戶重定向到網頁topics,函數reverse()根據指定的URL模型肯定URL return HttpResponseRedirect(reverse("learning_logs:topics")) context = {"form": form} return render(request, "learning_logs/new_topic.html", context)
建立Web應用程序時,將用到兩種主要數據請求類型:GET
請求和POST
請求。從這倆英文單詞能夠看出,若是隻從服務器讀取數據頁面,則使用GET
請求;若是要提交用戶填寫的表單,一般使用POST
請求。固然還有一些其餘的請求類型,但這個項目中沒有使用。本項目中處理表單都使用POST
方法。服務器
request.method
存儲了請求的類型(第7行代碼)。微信
當不是POST
請求時,咱們生成一個空表單傳遞給模板new_topic.html
,而後返回給用戶;當請求是POST
時,咱們從request.POST
這個變量中獲取用戶提交的數據,並暫存到form
變量中。
經過is_valid()
方法驗證表單數據是否知足要求:用戶是否填寫了全部必不可少的字段(表單字段默認都是必填的),且輸入的數據與字段類型是否一致。固然這些驗證都是Django自動進行的。若是表單有效,在經過form
的save()
方法存儲到數據庫,而後經過reverse()
函數獲取頁面topics
的URL,並將其傳遞給HTTPResponseRedirect()
以重定向到topics
頁面。若是表單無效,把這些數據從新傳回給用戶。
{% 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
,所以其基本結構和項目中的其餘頁面相同。第6行中,參數action
告訴服務器將提交的表單數據送到什麼位置去處理,參數method
讓瀏覽器以POST
請求的方式提交數據。
Django使用模板標籤csrf_token
(第7行)來防止攻擊者利用表單得到對服務器未經受權的訪問(跨站請求僞造)。
Django顯示錶單很是方便:只須要使用模板變量form.as_p
,修飾符as_p
讓Django以段落格式渲染全部表單元素,這是一種整潔地顯示錶單的簡單方法。
Django不自動建立提交表單的按鈕,需自行建立。
在頁面topics.html
中添加一個到頁面new_topic
的連接:
{% extends "learning_logs/base.html" %} {% block content %} -- snip -- <a href="{% url 'learning_logs:new_topic' %}">Add a new topic:</a> {% endblock content %}
如下是實際效果圖:
經過這個頁面,隨意添加幾個主題,以下:
和前面的步驟類似:建立條目表單,添加URL,添加視圖,添加模板,連接到頁面
建立一個與模型Entry相關聯的表單,但這個表單的自定義程度比TopicForm
要高些,依然是在剛纔建立的forms.py
中添加:
from .models import Topic, Entry class EntryForm(forms.ModelForm): class Meta: model = Entry fields = ["text"] labels = {"text": ""} widgets = {"text": forms.Textarea(attrs={"cols": 80})}
代碼中定義了屬性widgets
。小部件(widget)是一個HTML表單元素,如單行文本框、多行文本框或下拉列表。經過設置屬性widgets能夠覆蓋Django選擇的默認小部件。經過Django的forms.Textarea
定製字段「text"
的輸入小部件,將文本框的寬度設置爲80列,而不是默認的40列。
修改learning_logs/urls.py
:
urlpatterns = [ -- snip -- path("new_entry/<int:topic_id>/", views.new_entry, name="new_entry"), ]
該URL模式與形式爲http://localhost:8000/new_entry/topi_id/ 的URL匹配,其中topic_id
是主題的ID。
與函數new_topic()
很像:
from .forms import TopicForm, EntryForm 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)
new_entry()
的定義包含形參topic_id
,用於存儲從URL中得到的值。
在調用save()
時傳遞了參數commit=False
(第14行),它讓Django建立一個新的條目對象,但並不馬上提交數據庫,而是暫時存儲在變量new_entry
中,待爲這個新條目對象添加了屬性topic
以後再提交數據庫。
在重定向時,reverse()
函數中傳遞了兩個參數,URL模式的名稱以及列表args
,args
包含要包含在URL中的全部參數。
相似於new_topic
:
{% 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 %}
注意第4行代碼,改行代碼返回到特定主題頁面。
在顯示特定主題的頁面中添加到頁面new_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> -- snip -- {% endblock content %}
下圖是實際效果,請隨意添加一些條目:
建立一個頁面,讓用戶能編輯既有條目。順序是:添加URL,添加視圖,添加模板,連接到頁面。
修改learning_logs/urls.py
:
urlpatterns = [ -- snip -- path("edit_entry/<int:entry_id>/", views.edit_entry, name="edit_entry"), ]
from .models import Topic, Entry 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
請求時,經過參數instance=entry
建立EntryForm
實例,該參數讓Django建立一個表單,並使用既有條目對象中的信息填充它。處理POST
請求時,還傳入了data=request.POST
參數,Django根據POST中的相關數據對entry
進行修改。
{% 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 %}"> {% csrf_token %} {{ form.as_p }} <button name="submit">save changes</button> </form> {% endblock content %}
在顯示特定主題的頁面中,須要給每一個條目添加到頁面edit_entry.html
的連接,爲此,修改topic.html
:
-- snip -- {% 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> -- snip --
如下是實際效果圖:
如今開始創建一個用戶註冊和身份驗證系統。爲此將建立一個新的應用程序,其中包含處理用戶帳戶相關的全部功能。對Topic
模型也要作稍許修改,讓每一個主題都歸屬於特定用戶。
但願你們還記得如何使用startapp
命令還建立APP:
python manage.py startapp users
將APP添加到settings.py中
INSTALLED_APPS = [ -- snip -- "users.apps.UsersConfig", ]
在APP根目錄下建立urls.py
文件,並添加命名空間:
app_name = "users"
爲APP定義URL,修改項目根目錄中的urls.py
:
urlpatterns = [ path('admin/', admin.site.urls), path("", include("learning_logs.urls")), path("users/", include("users.urls")), ]
使用Django提供的默認登錄視圖,URL模式會有所不一樣。在users
中的urls.py
中添加以下代碼:
"""爲應用程序users定義URL模式""" from django.contrib.auth.views import login from django.urls import path app_name = "users" urlpatterns = [ # 登錄頁面 path("login/", login, {"template_name": "users/login.html"}, name="login"), ]
代碼中,咱們使用Django自帶的login視圖函數(注意,參數是login
,而不是views.login
)。從以前的例子能夠看出,咱們渲染模板的代碼都是在本身寫的視圖函數中。但這裏使用了自帶的視圖函數,沒法自行編寫進行渲染的代碼。因此,咱們還傳了一個字典給path
,告訴Django到哪裏查找咱們要用到的模板。注意,該模板在users
中,而不是在learning_logs
中。
在learning_log/users/templates/users
中建立login.html
:
{% extends "learning_logs/base.html" %} {% block content %} {% if form.errors %} <P>Your username and password didn't match. Please try again.</P> {% endif %} <form method="post" action="{% url 'users:login' %}"> {% csrf_token %} {{ form.as_p }} <button name="submit">log in</button> <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/> </form> {% endblock content %}
若是表單的errors
屬性被設置,則顯示一條提示帳號密碼錯誤的信息。
在base.html
中添加到登錄頁面的連接,讓全部頁面都包含它。將這個連接嵌套在一個 if
標籤中,用戶已登陸時隱藏掉該連接:
<p> <a href="{% url 'learning_logs:index' %}">Learning Log</a> - <a href="{% url 'learning_logs:topics' %}">Topics</a> {% if user.is_authenticated %} Hello, {{ user.username }} {% else %} <a href="{% url 'users:login' %}">log in</a> {% endif %} </p> {% block content %}{% endblock content %}
在Django身份驗證系統中,每一個模板均可使用變量user
,這個變量有一個is_authenticated
屬性:若是用戶已登陸,該屬性將爲True
,不然爲False
。
首先訪問localhost:8000/admin
註銷超級用戶,再訪問localhost:8000/users/login/
,獲得以下頁面:
並不爲註銷建立單獨的頁面,而是讓用戶單擊一個鏈接用於註銷並返回主頁。所以,須要作以下工做:註銷URL模式,新建視圖,連接到註銷視圖。
在users/urls.py
中添加與http://localhost:8000/users/logout/ 匹配的URL模式:
-- snip -- urlpatterns = [ -- snip -- path("logout/", views.logout_view, name="logout"), ]
編寫視圖函數logout_view()
,其中,直接調用Django自帶的logout()
函數,該函數要求request
做爲參數:
from django.contrib.auth import logout from django.http import HttpResponseRedirect from django.urls import reverse def logout_view(request): """註銷用戶""" logout(request) return HttpResponseRedirect(reverse("learning_logs:index"))
在base.html
中添加註銷連接:
-- snip -- {% if user.is_authenticated %} Hello, {{ user.username }} <a href="{% url 'users:logout' %}">log out</a> {% else %} <a href="{% url 'users:login' %}">log in</a> {% endif %} -- snip --
使用Django提供的表單UserCreationFrom
,但編寫本身的視圖函數和模板。URL->view->template->link
。
首先,建立註冊頁面的URL模式,修改users/urls.py
:
-- snip -- urlpatterns = [ -- snip -- path("register/", views.register, name="register"), ]
其次,建立視圖register()
:
from django.contrib.auth import logout, authenticate, login from django.contrib.auth.forms import UserCreationForm from django.shortcuts import render -- snip -- def register(request): """註冊新用戶""" if request.method != "POST": # 顯示空的註冊表單 form = UserCreationForm() else: # 處理填寫好的表單 form = UserCreationForm(data=request.POST) if form.is_valid(): new_user = form.save() # 讓用戶自動登錄,再重定向到主頁 # 註冊是要求輸入兩次密碼,因此有password1和password2 authenticated_user = authenticate(username=new_user.username, password=request.POST["password1"]) login(request, authenticated_user) return HttpResponseRedirect(reverse("learning_logs:index")) context = {"form": form} return render(request, "users/register.html", context)
以上代碼在用戶成功建立了用戶後會自動登錄,該功能由login()
函數實現。該函數將會爲經過了身份驗證的用戶對象建立會話(session)。最後上述代碼重定向到主頁。
而後,編寫註冊頁面的模板register.html:
{% extends "learning_logs/base.html" %} {% block content %} <form method="post" action="{% url 'users:register' %}"> {% csrf_token %} {{ form.as_p }} <button name="sumbit">register</button> <input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/> </form> {% endblock content %}
最後,在頁面中顯示註冊連接,修改base.html
,在用戶沒有登陸時顯示註冊連接:
-- snip -- {% if user.is_authenticated %} Hello, {{ user.username }} <a href="{% url 'users:logout' %}">log out</a> {% else %} <a href="{% url 'users:register' %}">register</a> - <a href="{% url 'users:login' %}">log in</a> {% endif %} -- snip --
下面是實際效果:
這是直接點register按鈕時的反饋,不過這裏有點疑惑,從上面的register.html中看到,其實代碼很簡單,但這裏有個浮動效果,並且在註冊模板中並無像前面那樣的form.errors模板標籤,但依然有未註冊成功時的反應,並且註冊的視圖函數也是本身寫的,並非用的自帶的註冊函數,因此不知道是否是和form.as_p有關。以後再慢慢研究吧,
用戶應該可以輸入其專有的數據,因此應該建立一個系統,肯定各項數據所屬的用戶,再限制對頁面的訪問,使得用戶只能使用本身的數據,即訪問控制。
Django提供了裝飾器@login_required
,使得能輕鬆實現用戶只能訪問本身能訪問的頁面。
限制對topics.html的訪問:
每一個主題都歸特定用戶全部,因此須要加限制,修改learning_logs/views.py
:
from django.contrib.auth.decorators import login_required @login_required def topics(request): """顯示全部的主題""" topics = Topic.objects.order_by("date_added") # 一個上下文字典,傳遞給模板 context = {"topics": topics} return render(request, "learning_logs/topics.html", context)
裝飾器也是一個函數,python在運行topics()
前會先運行login_required()
的代碼。
login_required()
函數檢查用戶是否登陸,僅當用戶已登陸時,Django才運行topics()
函數,若未登陸,就重定向到登錄界面。而爲了實現這個重定向,還須要修改項目的settings.py
文件,在該文件中添加這樣一個常量(其實也是變量),通常在文件末尾添加:
-- snip -- LOGIN_URL = '/users/login/'
全面限制對項目「學習筆記」的訪問:
Django能輕鬆地限制對頁面的訪問,但得本身設計須要限制哪些頁面。通常先肯定哪些頁面不須要保護,再限制對其餘頁面的訪問。在該項目中,咱們不限制對主頁、註冊頁面和註銷連接的訪問,其餘頁面均限制。在learning_logs/views.py
中,除了index()
外,每一個視圖函數都加上@login_required
。
爲了禁止用戶訪問其餘用戶的數據,須要將用戶與數據關聯。只須要將最高層的數據關聯到用戶,這樣更低層的數據將自動關聯到用戶。下面修改Topic
模型和相關視圖:
from django.contrib.auth.models import User from django.db import models class Topic(models.Model): """用戶學習的主題""" text = models.CharField(max_length=200) date_added = models.DateTimeField(auto_now_add=True) owner = models.ForeignKey(User, on_delete=models.CASCADE) -- snip --
修改模型後,還須要遷移數據庫。此時,須要將主題與用戶關聯。這裏並無經過代碼進行關聯,咱們在遷移數據庫時手動進行關聯。爲此,咱們須要先知道有哪些用戶,以及這些用戶的ID。咱們經過Django shell
查詢用戶信息,固然也能夠直接查看數據庫,這裏再也不演示。咱們將主題都關聯到超級用戶ll_admin
上,它的ID是1。如今咱們執行數據遷移:
(venv)learning_logs$ python manage.py makeimgrations learning_logs You are trying to add a non-nullable field 'owner' to topic without a default; we can't do that (the database needs something to populate existing rows). Please select a fix: 1) Provide a one-off default now (will be set on all existing rows with a null value for this column) 2) Quit, and let me add a default in models.py Select an option: 1 Please enter the default value now, as valid Python The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now Type 'exit' to exit this prompt >>> 1 Migrations for 'learning_logs': learning_logs\migrations\0003_topic_owner.py - Add field owner to topic
Django指出試圖給既有模型Topic
添加一個必不可少(不可爲空)的字段,而該字段沒有默認值,須要咱們採起措施:要麼如今提供默認值,要麼退出並在models.py
中添加默認值。咱們選擇了直接輸入默認值。接下來,Django使用這個值來遷移數據庫,並生成了遷移文件0003_topic_owner.py
,它在模型Topic中添加字段owner
。
如今執行遷移命令:
(venv)learning_logs$ python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, learning_logs, sessions Running migrations: Applying learning_logs.0003_topic_owner... OK
執行後能夠在Django shell
中驗證是否遷移成功,這裏再也不驗證。
目前無論以哪一個用戶身份登陸,都能看到全部主題。如今咱們添加限制,讓用戶只能看到本身的主題。在views.py
中,對topics()
作以下修改:
-- snip -- @login_required def topics(request): """顯示全部的主題""" topics = Topic.objects.filter(owner=request.user).order_by("date_added") # 一個上下文字典,傳遞給模板 context = {"topics": topics} return render(request, "learning_logs/topics.html", context) -- snip --
用戶登陸後,request
對象將有一個user
屬性,這個屬性存儲了有關該用戶的信息。第5行代碼讓Django只從數據庫中讀取特定用戶的數據。
上述代碼作到了登陸後只顯示相應用戶的數據,可是,若是登陸後直接經過URL訪問,如直接輸入http://localhost:8000/topics/1/ ,依然能夠訪問不屬於本身的特定主題頁面。下面修改views.py
中的topic()
函數來加以限制:
from django.http import HttpResponseRedirect, Http404 @login_required def topic(request, topic_id): """顯示單個主題及其全部的條目""" topic = Topic.objects.get(id=topic_id) # 確認請求的主題屬於當前用戶 if topic.owner != request.user: raise Http404 -- snip --
此時用戶也能夠像上面同樣,登錄後直接經過URL來訪問edit_entry.html
,如今咱們對這個頁面也加以限制:
@login_required def edit_entry(request, entry_id): """編輯既有條目""" entry = Entry.objects.get(id=entry_id) topic = entry.topic if topic.owner != request.user: raise Http404 -- snip --
當前用於添加新主題的頁面存在問題,由於它沒有將新主題關聯到特定用戶。若是此時嘗試添加新主題,將看到錯誤信息IntegrityError
,指出learning_logs_topic.user_id
不能爲NULL
,下面修改new_topic()
函數:
@login_required def new_topic(request): """添加新主題""" if request.method != "POST": # 爲提價數據:建立一個新表單 form = TopicForm() else: # POST提交的數據,對數據進行處理 form = TopicForm(request.POST) if form.is_valid(): # 添加新主題時關聯到特定用戶 new_topic = form.save(commit=False) new_topic.owner = request.user new_topic.save() # 該類將用戶重定向到網頁topics,函數reverse()根據指定的URL模型肯定URL return HttpResponseRedirect(reverse("learning_logs:topics")) context = {"form": form} return render(request, "learning_logs/new_topic.html", context)
如今,這個項目容許任何用戶註冊,而每一個用戶想添加多少新主題均可以,每一個用戶只能訪問本身的數據,不管是查看數據、輸入新數據仍是修改舊數據時都是如此。
本篇主要講述瞭如何使用表單來讓用戶添加新主題、添加新條目和編輯既有條目;如何實現註冊,登陸與註銷,如何使用裝飾器來限制訪問,如何對用戶數據進行保護等。下一篇中,咱們將使這個項目更漂亮,而且部署到服務器上。
迎你們關注個人微信公衆號"代碼港" & 我的網站 www.vpointer.net ~