開發(fā)系統(tǒng)和開發(fā)IDE
開發(fā)系統(tǒng): Ubuntu 16.0.4 LTS
開發(fā)IDE: Visual Studio Code 版本: 1.32.3
Python版本: Python3
依賴: Django 2.2
資料《Python編程從入門到實踐》書籍
鏈接:https://pan.baidu.com/s/1USkqvL2dLU3Q9XplVaGQJg
提取碼:zoyc
GitHub:
https://github.com/lichangke/Python3_Project/tree/master/learning_log
Web應(yīng)用程序 - Django入門
http://www.lxweimin.com/p/b3267d16c245
2. 用戶賬戶
Web應(yīng)用程序的核心是讓任何用戶都能夠注冊賬戶并能夠使用它。將創(chuàng)建一些表單, 讓用戶能夠添加主題和條目, 以及編輯既有的條目。
然后, 將實現(xiàn)一個用戶身份驗證系統(tǒng)。 你將創(chuàng)建一個注冊頁面, 供用戶創(chuàng)建賬戶, 并讓有些頁面只能供已登錄的用戶訪問。 接下來, 將修改一些視圖函數(shù),使得用戶只能看到自己的數(shù)據(jù)。
2.1 讓用戶能夠輸入數(shù)據(jù)
階段代碼:GitHub learning_log_2.1_讓用戶能夠輸入數(shù)據(jù)
不包括虛擬環(huán)境ll_env文件夾下文件
建立用于創(chuàng)建用戶賬戶的身份驗證系統(tǒng)之前, 先來添加幾個頁面, 讓用戶能夠輸入數(shù)據(jù)。 將讓用戶能夠添加新主題、 添加新條目以及編輯既有條目。
2.1.1 添加新主題
urls -> views -> html
首先來讓用戶能夠添加新主題。 創(chuàng)建基于表單的頁面的方法幾乎與前面創(chuàng)建網(wǎng)頁一樣: 定義一個URL, 編寫一個視圖函數(shù)并編寫一個模板。 一個主要差別是, 需要導(dǎo)入包含表單的模塊forms.py。
1. 用于添加主題的表單
讓用戶輸入并提交信息的頁面都是表單, 那怕它看起來不像表單。 用戶輸入信息時, 需要進行驗證, 確認提供的信息是正確的數(shù)據(jù)類型。然后, 再對這些有效信息進行處理, 并將其保存到數(shù)據(jù)庫的合適地方。 這些工作很多都是由Django自動完成的。
創(chuàng)建一個名為forms.py的文件, 將其存儲到models.py所在的目錄中
models.py
from django import forms
from .models import Topic
# 讓用戶輸入并提交信息的頁面都是表單, 那怕它看起來不像表單。
# 創(chuàng)建表單的最簡單方式是使用ModelForm, 它根據(jù)在模型中的信息自動創(chuàng)建表單。
class TopicForm(forms.ModelForm): # 定義了一個名為TopicForm 的類, 它繼承了forms.ModelForm 。
class Meta:
model = Topic # 根據(jù)模型Topic 創(chuàng)建一個表單
fields = ['text'] # 該表單只包含字段text
labels = {'text': ''} # 讓Django不要為字段text 生成標(biāo)簽。
2. URL模式new_topic
這個新網(wǎng)頁的URL應(yīng)簡短而具有描述性, 因此當(dāng)用戶要添加新主題時, 將切換到http://localhost:8000/new_topic/。 下面是網(wǎng)頁new_topic 的URL模式, 將其添加到learning_logs/urls.py中:
urls.py
--snip--
urlpatterns = [
--snip--
# 用于添加新主題的網(wǎng)頁
url(r'^new_topic/$', views.new_topic, name='new_topic'),
]
這個URL模式將請求交給視圖函數(shù)new_topic() , 接下來將編寫這個函數(shù)。
3. 視圖函數(shù)new_topic()
函數(shù)new_topic() 需要處理兩種情形: 剛進入new_topic 網(wǎng)頁(在這種情況下, 它應(yīng)顯示一個空表單) ; 對提交的表單數(shù)據(jù)進行處理, 并將用戶重定向到網(wǎng)頁topics
views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect
from .models import Topic
from .forms import TopicForm
from django.urls import reverse
# from django.core.urlresolvers import reverse
'''
https://stackoverflow.com/questions/43139081/importerror-no-module-named-django-core-urlresolvers
Django 2.0 removes the django.core.urlresolvers module, which was moved to django.urls in version 1.10.
You should change any import to use django.urls instead, like this:
from django.urls import reverse
'''
--snip--
# 函數(shù)new_topic() 需要處理兩種情形: 剛進入new_topic 網(wǎng)頁(在這種情況下, 它應(yīng)顯示一個空表單) ; 對提交的表單數(shù)據(jù)進行處理, 并將用戶重定向到網(wǎng)頁topics
def new_topic(request):
"""添加新主題"""
if request.method != 'POST':
# 未提交數(shù)據(jù): 創(chuàng)建一個新表單
form = TopicForm() # 如果請求方法不是POST, 請求就可能是GET, 因此我們需要返回一個空表單
else:
if form.is_valid(): # 必須先通過檢查確定它們是有效的
form.save() # 表單中的數(shù)據(jù)寫入數(shù)據(jù)庫
# 函數(shù)reverse() 根據(jù)指定的URL模型確定URL, 這意味著Django將在頁面被請求時生成URL。
# 調(diào)用HttpResponseRedirect() 將用戶重定向到顯示新增條目所屬主題的頁面
return HttpResponseRedirect(reverse('learning_logs:topics'))
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
'''
創(chuàng)建Web應(yīng)用程序時, 將用到的兩種主要請求類型是GET請求和POST請求。 對于只是從服務(wù)器讀取數(shù)據(jù)的頁面, 使用GET請求; 在用戶需要通過表單提交信息時, 通常使用POST
請求。 處理所有表單時, 我們都將指定使用POST方法。 還有一些其他類型的請求, 但這個項目沒有使用。
函數(shù)new_topic() 將請求對象作為參數(shù)。 用戶初次請求該網(wǎng)頁時, 其瀏覽器將發(fā)送GET請求; 用戶填寫并提交表單時, 其瀏覽器將發(fā)送POST請求。 根據(jù)請求的類型, 我們可以
確定用戶請求的是空表單(GET請求) 還是要求對填寫好的表單進行處理(POST請求) 。
'''
4. 模板new_topic
創(chuàng)建新模板new_topic.html, 用于顯示剛創(chuàng)建的表單
new_topic.html
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Add a new topic:</p>
<!--定義了一個HTML表單-->
<!--實參action 告訴服務(wù)器將提交的表單數(shù)據(jù)發(fā)送到哪里, 這里我們將它發(fā)回給視圖函數(shù)new_topic() 。 實參method 讓瀏覽器以POST請求的方式提交數(shù)據(jù)。-->
<form action="{% url 'learning_logs:new_topic' %}" method='post'>
{% csrf_token %} <!--防止攻擊者利用表單來獲得對服務(wù)器未經(jīng)授權(quán)的訪問-->
{{ form.as_p }} <!--顯示表單修飾符as_p 讓Django以段落格式渲染所有表單元素, 這是一種整潔地顯示表單的簡單方式-->
<button name="submit">add topic</button> <!--Django不會為表單創(chuàng)建提交按鈕, 因此定義了一個這樣的按鈕-->
</form>
{% endblock content %}
5. 鏈接到頁面new_topic
在頁面topics 中添加一個到頁面new_topic 的鏈接:
topics.html
{% 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 %}
鏈接放在了既有主題列表的后面。 下圖顯示了生成的表單。
2.1.2 添加新條目
urls -> views -> html 添加網(wǎng)頁步驟
用戶可以添加新主題了, 但他們還想添加新條目。 將再次定義URL, 編寫視圖函數(shù)和模板, 并鏈接到添加新條目的網(wǎng)頁。 但在此之前, 需要在forms.py中再添加一個類。
1. 用于添加新條目的表單
創(chuàng)建一個與模型Entry 相關(guān)聯(lián)的表單
forms.py
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
--snip--
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['text']
labels = {'text': ''}
widgets = {'text': forms.Textarea(attrs={'cols': 80})}
'''
定義了屬性widgets 。 小部件 (widget) 是一個HTML表單元素, 如單行文本框、 多行文本區(qū)域或下拉列表。 通過設(shè)置屬性widgets , 可覆蓋Django選擇的默認小
部件。 通過讓Django使用forms.Textarea , 我們定制了字段'text' 的輸入小部件, 將文本區(qū)域的寬度設(shè)置為80列, 而不是默認的40列。 這給用戶提供了足夠的空間, 可以
編寫有意義的條目。
'''
2. URL模式new_entry
添加新條目的頁面的URL模式中, 需要包含實參topic_id , 因為條目必須與特定的主題相關(guān)聯(lián)。 該URL模式如下, 將它添加到了learning_logs/urls.py中
urls.py
--snip--
urlpatterns = [
--snip--
# 用于添加新條目的頁面
re_path(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'),
]
3. 視圖函數(shù)new_entry()
views.py
--snip--
from .models import Topic
from .forms import TopicForm,EntryForm
from django.urls import reverse
--snip--
def new_entry(request, topic_id):
"""在特定的主題中添加新條目"""
topic = Topic.objects.get(id=topic_id)
if request.method != 'POST':
# 未提交數(shù)據(jù),創(chuàng)建一個空表單
form = EntryForm()
else:
# POST提交的數(shù)據(jù),對數(shù)據(jù)進行處理
form = EntryForm(data=request.POST)
if form.is_valid():
# 調(diào)用save() 時, 傳遞了實參commit=False , 讓Django創(chuàng)建一個新的條目對象, 并將其存儲到new_entry 中, 但不將它保存到數(shù)據(jù)庫中。
new_entry = form.save(commit=False)
new_entry.topic = topic # 將new_entry的屬性topic 設(shè)置為在這個函數(shù)開頭從數(shù)據(jù)庫中獲取的主題
new_entry.save() # 調(diào)用save() , 且不指定任何實參。 這將把條目保存到數(shù)據(jù)庫, 并將其與正確的主題相關(guān)聯(lián)。
return HttpResponseRedirect(reverse('learning_logs:topic',args=[topic_id]))
context = {'topic': topic, 'form': form}
return render(request, 'learning_logs/new_entry.html', context)
4. 模板new_entry
new_entry.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 %}
5. 鏈接到頁面new_entry
在顯示特定主題的頁面中添加到頁面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>
<ul>
--snip—
</ul>
{% endblock content %}
下圖顯示了頁面new_entry
2.1.3 編輯條目
urls -> views -> html 添加網(wǎng)頁步驟
創(chuàng)建一個頁面, 讓用戶能夠編輯既有的條目。
1. URL模式edit_entry
這個頁面的URL需要傳遞要編輯的條目的ID。 修改后的learning_logs/urls.py如下
urls.py
--snip--
# https://docs.djangoproject.com/en/2.2/ref/urls/#module-django.urls.conf
urlpatterns = [
# 主頁
path('', views.index, name='index'), # Django將在文件views.py中查找函數(shù)index()
# 顯示所有的主題
path('topics/',views.topics,name = 'topics'),
# 特定主題的詳細頁面
# use a regular expression, you can use re_path(). https://stackoverflow.com/questions/47661536/django-2-0-path-error-2-0-w001-has-a-route-that-contains-p-begins-wit
re_path(r'^topics/(?P<topic_id>\d+)/$', views.topic, name='topic'), # ?P<topic_id> 將匹配的值存儲到topic_id 中; 而表達式\d+ 與包含在兩個斜桿內(nèi)的任何數(shù)字都匹配, 不管這個數(shù)字為多少位。
# 用于添加新主題的網(wǎng)頁
path('new_topic/', views.new_topic, name = 'new_topic'),
# 用于添加新條目的頁面
re_path(r'^new_entry/(?P<topic_id>\d+)/$', views.new_entry, name='new_entry'),
# 用于編輯條目的頁面
re_path(r'^edit_entry/(?P<entry_id>\d+)/$',views.edit_entry,name='edit_entry')
]
在URL(如http://localhost:8000/edit_entry/1/) 中傳遞的ID存儲在形參entry_id 中。 這個URL模式將預(yù)期匹配的請求發(fā)送給視圖函數(shù)edit_entry()
2. 視圖函數(shù)edit_entry()
頁面edit_entry 收到GET請求時, edit_entry() 將返回一個表單, 讓用戶能夠?qū)l目進行編輯。 該頁面收到POST請求(條目文本經(jīng)過修訂) 時, 它將修改后的文本保存到數(shù)據(jù)庫中:
views.py
--snip--
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
--snip--
def edit_entry(request, entry_id):
"""編輯既有條目"""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if request.method != 'POST':
# 初次請求, 使用當(dāng)前條目填充表單
form = EntryForm(instance=entry)
else:
# POST提交的數(shù)據(jù), 對數(shù)據(jù)進行處理
# 讓Django根據(jù)既有條目對象創(chuàng)建一個表單實例, 并根據(jù)request.POST 中的相關(guān)數(shù)據(jù)對其進行修改
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)
3. 模板edit_entry
edit_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'> <!--實參action 將表單發(fā)回給函數(shù)edit_entry() 進行處理-->
{% csrf_token %}
{{ form.as_p }}
<button name="submit">save changes</button>
</form>
{% endblock content %}
4. 鏈接到頁面edit_entry
在顯示特定主題的頁面中, 需要給每個條目添加到頁面edit_entry 的鏈接:
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--
下圖顯示了包含這些鏈接時, 顯示特定主題的頁面是什么樣的
2.2 創(chuàng)建用戶賬戶
階段代碼:GitHub learning_log_2.2_創(chuàng)建用戶賬戶
不包括虛擬環(huán)境ll_env文件夾下文件
將建立一個用戶注冊和身份驗證系統(tǒng), 讓用戶能夠注冊賬戶, 進而登錄和注銷。 將創(chuàng)建一個新的應(yīng)用程序, 其中包含與處理用戶賬戶相關(guān)的所有功能。 還將對模型Topic 稍做修改, 讓每個主題都歸屬于特定用戶。
2.2.1 應(yīng)用程序users
步驟:
startapp創(chuàng)建應(yīng)用程序 -> 將應(yīng)用程序添加到settings.py -> 包含應(yīng)用程序users 的URL
先使用命令startapp 來創(chuàng)建一個名為users 的應(yīng)用程序
python manage.py startapp users
1. 將應(yīng)用程序users 添加到settings.py中
在settings.py中, 我們需要將這個新的應(yīng)用程序添加到INSTALLED_APPS 中
settings.py
--snip--
INSTALLED_APPS = (
--snip--
# 我的應(yīng)用程序
'learning_logs',
'users',
)-
-snip
2. 包含應(yīng)用程序users 的URL
需要修改項目根目錄中的urls.py, 使其包含為應(yīng)用程序users 定義的URL:
urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),# 該模塊定義了可在管理網(wǎng)站中請求的所有URL
path('', include('learning_logs.urls', namespace='learning_logs')),
# 代碼包含實參namespace , 讓我們能夠?qū)earning_logs 的URL同項目中的其他URL區(qū)分開來
path('users/',include('users.urls', namespace='users')), # 這行代碼與任何以單詞users打頭的URL(如http://localhost:8000/users/login/) 都匹配
]
2.2.2 登錄頁面
首先來實現(xiàn)登錄頁面的功能。 為此, 將使用Django提供的默認登錄視圖, 因此URL模式會稍有不同。 在目錄learning_log/users/中, 新建一個名為urls.py的文件, 并在其中添加如下代碼:
urls.py
"""為應(yīng)用程序users定義URL模式"""
# 非 from django.conf.urls import url
from django.urls import path,re_path
from django.contrib.auth.views import LoginView
# from django.contrib.auth.views import login In django-2.1, the old function-based views have been removed,
from . import views
app_name= 'users'# 不能少
urlpatterns = [
# 登錄頁面
# re_path(r'^login/$', login, {'template_name': 'users/login.html'},name='login'),
re_path(r'^login/$',LoginView.as_view(template_name='users/login.html'),name='login')
]
注意 注釋部分為原書中代碼,Django版本不同需使用新的方式。app_name= 'users'# 不能少
1. 模板login.html
用戶請求登錄頁面時, Django將使用其默認視圖login , 但依然需要為這個頁面提供模板。 為此, 在目錄learning_log/users/中, 創(chuàng)建一個名為templates的目錄, 并在其中創(chuàng)建一個名為users的目錄。 以下是模板login.html, 你應(yīng)將其存儲到目錄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' %}" />
<!--包含了一個隱藏的表單元素——'next' , 其中的實參value 告訴Django在用戶成功登錄后將其重定向到什么地方——在這里是主頁。-->
</form>
{% endblock content %}
2. 鏈接到登錄頁面
在base.html中添加到登錄頁面的鏈接, 讓所有頁面都包含它。 用戶已登錄時, 我們不想顯示這個鏈接, 因此將它嵌套在一個{% if %} 標(biāo)簽中
base.html
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics' %}">Topics</a>
<!--在Django身份驗證系統(tǒng)中, 每個模板都可使用變量user , 這個變量有一個is_authenticated 屬性: 如果用戶已登錄, 該屬性將為True , 否則為False 。-->
{% if user.is_authenticated %}
Hello, {{ user.username }}.
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
3. 使用登錄頁面
前面建立了一個用戶賬戶, 下面來登錄一下, 看看登錄頁面是否管用。 請訪問http://localhost:8000/admin/, 如果你依然是以管理員的身份登錄的, 請在頁眉上找到注銷鏈接并單擊它。
訪問http://localhost:8000/users/login/, 你將看到類似于下圖所示的登錄頁面。
2.2.3 注銷
需要提供一個讓用戶注銷的途徑。 我們不創(chuàng)建用于注銷的頁面, 而讓用戶只需單擊一個鏈接就能注銷并返回到主頁。 為此, 將為注銷鏈接定義一個URL模式, 編寫一個視圖函數(shù), 并在base.html中添加一個注銷鏈接
1. 注銷URL
下面的代碼為注銷定義了URL模式, 該模式與URL http://locallwst:8000/users/logout/匹配。 修改后的users/urls.py如下
urls.py
--snip--
urlpatterns = [
# 登錄頁面
# re_path(r'^login/$', login, {'template_name': 'users/login.html'},name='login'),
re_path(r'^login/$',LoginView.as_view(template_name='users/login.html'),name='login'),
# 注銷
re_path(r'^logout/$', views.logout_view, name='logout'),
]
這
個URL模式將請求發(fā)送給函數(shù)logout_view() 。 這樣給這個函數(shù)命名, 旨在將其與我們將在其中調(diào)用的函數(shù)logout() 區(qū)分開來(請確保你修改的是users/urls.py, 而不是learning_log/ urls.py) 。
2. 視圖函數(shù)logout_view()
函數(shù)logout_view() 很簡單: 只是導(dǎo)入Django函數(shù)logout() , 并調(diào)用它, 再重定向到主頁。 請打開users/views.py, 并輸入下面的代碼
views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import logout
# Create your views here.
def logout_view(request):
"""注銷用戶"""
logout(request)
return HttpResponseRedirect(reverse('learning_logs:index'))
3. 鏈接到注銷視圖
需要添加一個注銷鏈接。 我們在base.html中添加這種鏈接, 讓每個頁面都包含它; 將它放在標(biāo)簽{% if user.is_authenticated %} 中, 使得僅當(dāng)用戶登錄后才能看到它:
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--
下圖顯示了用戶登錄后看到的主頁
2.2.4 注冊頁面
下面來創(chuàng)建一個讓新用戶能夠注冊的頁面。 將使用Django提供的表單UserCreationForm , 但編寫自己的視圖函數(shù)和模板。
1. 注冊頁面的URL模式
下面的代碼定義了注冊頁面的URL模式, 它也包含在users/urls.py中
urls.py
--snip--
urlpatterns = [
# 登錄頁面
--snip--
# 注冊頁面
re_path(r'^register/$', views.register, name='register'), # 與URL http://localhost:8000/users/register/匹配, 并將請求發(fā)送給我們即將編寫的函數(shù)register()
]
這個模式與URL http://localhost:8000/users/register/匹配, 并將請求發(fā)送給我們即將編寫的函數(shù)register() 。
2. 視圖函數(shù)register()
在注冊頁面首次被請求時, 視圖函數(shù)register() 需要顯示一個空的注冊表單, 并在用戶提交填寫好的注冊表單時對其進行處理。 如果注冊成功, 這個函數(shù)還需讓用戶自動登錄。 請在users/views.py中添加如下代碼
views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import login,logout,authenticate
from django.contrib.auth.forms import UserCreationForm
# Create your views here.
def logout_view(request):
--snip--
def register(request):
"""注冊新用戶"""
if request.method != 'POST':
# 顯示空的注冊表單
form = UserCreationForm()
else:
# 處理填寫好的表單
form = UserCreationForm(data=request.POST)
if form.is_valid():
new_user = form.save()
# 讓用戶自動登錄, 再重定向到主頁
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)
3. 注冊模板
注冊頁面的模板與登錄頁面的模板類似, 請務(wù)必將其保存到login.html所在的目錄中
register.html
{% extends "learning_logs/base.html" %}
{% block content %}
<form method="post" action="{% url 'users:register' %}">
{% csrf_token %}
{{ form.as_p }}
<button name="submit">register</button>
<input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
</form>
{% endblock content %}
使用了方法as_p , 讓Django在表單中正確地顯示所有的字段, 包括錯誤消息——如果用戶沒有正確地填寫表單。
4. 鏈接到注冊頁面
添加這樣的代碼, 即在用戶沒有登錄時顯示到注冊頁面的鏈接
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--
如下圖所示
2.3 讓用戶擁有自己的數(shù)據(jù)
階段代碼:GitHub learning_log_2.3_讓用戶擁有自己的數(shù)據(jù)
不包括虛擬環(huán)境ll_env文件夾下文件
用戶應(yīng)該能夠輸入其專有的數(shù)據(jù), 因此將創(chuàng)建一個系統(tǒng), 確定各項數(shù)據(jù)所屬的用戶, 再限制對頁面的訪問, 讓用戶只能使用自己的數(shù)據(jù)
將修改模型Topic , 讓每個主題都歸屬于特定用戶。 這也將影響條目, 因為每個條目都屬于特定的主題。 先來限制對一些頁面的訪問。
2.3.1 使用@login_required 限制訪問
Django提供了裝飾器@login_required , 讓你能夠輕松地實現(xiàn)這樣的目標(biāo): 對于某些頁面, 只允許已登錄的用戶訪問它們。 裝飾器 (decorator) 是放在函數(shù)定義前面的指令, Python在函數(shù)運行前, 根據(jù)它來修改函數(shù)代碼的行為。
1. 限制對topics 頁面的訪問
每個主題都歸特定用戶所有, 因此應(yīng)只允許已登錄的用戶請求topics 頁面。 為此, 在learning_logs/views.py中添加如下代碼
views.py
--snip--
from django.contrib.auth.decorators import login_required
--snip--
#Django提供了裝飾器@login_required , 讓你能夠輕松地實現(xiàn)這樣的目標(biāo): 對于某些頁面, 只允許已登錄的用戶訪問它們
@login_required
def topics(request):
"""顯示所有的主題"""
--snip--
導(dǎo)入了函數(shù)login_required() 。 我們將login_required() 作為裝飾器用于視圖函數(shù)topics() ——在它前面加上符號@ 和login_required , 讓Python在運行topics() 的代碼前先運行l(wèi)ogin_required() 的代碼。
login_required() 的代碼檢查用戶是否已登錄, 僅當(dāng)用戶已登錄時, Django才運行topics() 的代碼。 如果用戶未登錄, 就重定向到登錄頁面。
為實現(xiàn)這種重定向, 需要修改settings.py, 讓Django知道到哪里去查找登錄頁面。 請在settings.py末尾添加如下代碼
settings.py
--snip--
# 我的設(shè)置
LOGIN_URL = '/users/login/'
如果未登錄的用戶請求裝飾器@login_required 的保護頁面, Django將重定向到settings.py中的LOGIN_URL 指定的URL
2. 全面限制對項目“學(xué)習(xí)筆記”的訪問
Django讓你能夠輕松地限制對頁面的訪問, 但你必須針對要保護哪些頁面做出決定。 最好先確定項目的哪些頁面不需要保護, 再限制對其他所有頁面的訪問。 可以輕松地修改過于嚴格的訪問限制, 其風(fēng)險比不限制對敏感頁面的訪問更低。
在項目“學(xué)習(xí)筆記”中, 將不限制對主頁、 注冊頁面和注銷頁面的訪問, 并限制對其他所有頁面的訪問。
在下面的learning_logs/views.py中, 對除index() 外的每個視圖都應(yīng)用了裝飾器@login_required
views.py
--snip--
@login_required
def topics(request):
--snip--
@login_required
def topic(request, topic_id):
--snip--
@login_required
def new_topic(request):
--snip--
@login_required
def new_entry(request, topic_id):
--snip--
@login_required
def edit_entry(request, entry_id):
--snip
2.3.2 將數(shù)據(jù)關(guān)聯(lián)到用戶
需要將數(shù)據(jù)關(guān)聯(lián)到提交它們的用戶。 我們只需將最高層的數(shù)據(jù)關(guān)聯(lián)到用戶, 這樣更低層的數(shù)據(jù)將自動關(guān)聯(lián)到用戶。 例如, 在項目“學(xué)習(xí)筆記”中, 應(yīng)用程序的最高層數(shù)據(jù)是主題, 而所有條目都與特定主題相關(guān)聯(lián)。 只要每個主題都歸屬于特定用戶, 我們就能確定數(shù)據(jù)庫中每個條目的所有者。
修改模型Topic , 在其中添加一個關(guān)聯(lián)到用戶的外鍵。 這樣做后, 我們必須對數(shù)據(jù)庫進行遷移。 最后, 我們必須對有些視圖進行修改, 使其只顯示與當(dāng)前登錄的用戶相關(guān)
聯(lián)的數(shù)據(jù)
1. 修改模型Topic
對models.py的修改只涉及兩行代碼:
models.py
from django.db import models
from django.contrib.auth.models import User
class Topic(models.Model):
''' 用戶學(xué)習(xí)的主題'''
# https://docs.djangoproject.com/en/2.2/ref/models/fields/#charfield
text = models.CharField(max_length = 200) # 屬性text是一個CharField——由字符或文本組成的數(shù)據(jù)
# https://docs.djangoproject.com/en/2.2/ref/models/fields/#datetimefield
date_added = models.DateTimeField(auto_now_add=True) # 實參auto_add_now=True 讓Django將這個屬性自動設(shè)置成當(dāng)前日期和時間。
owner = models.ForeignKey(User,on_delete=models.CASCADE) #
def __str__(self):
"""返回模型的字符串表示"""
return self.text
class Entry(models.Model):
--snip--
2. 確定當(dāng)前有哪些用戶
遷移數(shù)據(jù)庫時, Django將對數(shù)據(jù)庫進行修改, 使其能夠存儲主題和用戶之間的關(guān)聯(lián)。 為執(zhí)行遷移, Django需要知道該將各個既有主題關(guān)聯(lián)到哪個用戶。 最簡單的辦法是, 將既有主題都關(guān)聯(lián)到同一個用戶, 如超級用戶。 為此, 我們需要知道該用戶的ID。
查看已創(chuàng)建的所有用戶的ID。 為此, 啟動一個Django shell會話, 并執(zhí)行如下命令:
python manage.py shell
3. 遷移數(shù)據(jù)庫
知道用戶ID后, 就可以遷移數(shù)據(jù)庫了
python manage.py makemigrations learning_logs
為將所有既有主題都關(guān)聯(lián)到管理用戶ll_admin, 我輸入了用戶ID值2。并非必須使用超級用戶, 而可使用已創(chuàng)建的任何用戶的ID。 接下來, Django使用這個值來遷移數(shù)據(jù)庫, 并生成了遷移文件0003_topic_owner.py, 它在模型Topic 中添加字段owner 。
現(xiàn)在可以執(zhí)行遷移了。 為此, 在活動的虛擬環(huán)境中執(zhí)行下面的命令
python manage.py migrate
為驗證遷移符合預(yù)期, 可在shell會話中像下面這樣做:
2.3.3 只允許用戶訪問自己的主題
當(dāng)前, 不管你以哪個用戶的身份登錄, 都能夠看到所有的主題。 改變這種情況, 只向用戶顯示屬于自己的主題
在views.py中, 對函數(shù)topics() 做如下修改
views.py
--snip--
# Django提供了裝飾器@login_required , 讓你能夠輕松地實現(xiàn)這樣的目標(biāo): 對于某些頁面, 只允許已登錄的用戶訪問它們
@login_required
def topics(request):
"""顯示所有的主題"""
# topics = Topic.objects.order_by('date_added') # 查詢數(shù)據(jù)庫——請求提供Topic 對象, 并按屬性date_added 對它們進行排序
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics': topics} # 一個將要發(fā)送給模板的上下文。 上下文是一個字典, 其中的鍵是我們將在模板中用來訪問數(shù)據(jù)的名稱, 而值是我們要發(fā)送給模板的數(shù)據(jù)。
return render(request, 'learning_logs/topics.html', context)
--snip--
要查看結(jié)果, 以所有既有主題關(guān)聯(lián)到的用戶的身份登錄, 并訪問topics頁面, 你將看到所有的主題。 然后, 注銷并以另一個用戶的身份登錄, topics頁面將不會列出任何主題。
2.3.4 保護用戶的主題
還沒有限制對顯示單個主題的頁面的訪問, 因此任何已登錄的用戶都可輸入類似于http://localhost:8000/topics/1/的URL, 來訪問顯示相應(yīng)主題的頁面
為修復(fù)這種問題, 我們在視圖函數(shù)topic() 獲取請求的條目前執(zhí)行檢查:
views.py
--snip--
from django.http import HttpResponseRedirect, Http404
--snip--
@login_required
def topic(request, topic_id):
"""顯示單個主題及其所有的條目"""
topic = Topic.objects.get(id=topic_id)
# 確認請求的主題屬于當(dāng)前用戶
if topic.owner != request.user:
raise Http404
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
--snip--
導(dǎo)入了異常Http404,并在用戶請求它不能查看的主題時引發(fā)這個異常
2.3.5 保護頁面edit_entry
頁面edit_entry 的URL為http://localhost:8000/edit_entry/entry_id / , 其中 entry_id 是一個數(shù)字。 下面來保護這個頁面, 禁止用戶通過輸入類似于前面的URL來訪問其他用戶的條目
views.py
--snip--
@login_required
def edit_entry(request, entry_id):
"""編輯既有條目"""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if topic.owner != request.user: # 保護頁面edit_entry
raise Http404
if request.method != 'POST':
# 初次請求, 使用當(dāng)前條目填充表單
form = EntryForm(instance=entry)
else:
# POST提交的數(shù)據(jù), 對數(shù)據(jù)進行處理
# 讓Django根據(jù)既有條目對象創(chuàng)建一個表單實例, 并根據(jù)request.POST 中的相關(guān)數(shù)據(jù)對其進行修改
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)
獲取指定的條目以及與之相關(guān)聯(lián)的主題, 然后檢查主題的所有者是否是當(dāng)前登錄的用戶, 如果不是, 就引發(fā)Http404 異常
2.3.6 將新主題關(guān)聯(lián)到當(dāng)前用戶
當(dāng)前, 用于添加新主題的頁面存在問題, 因此它沒有將新主題關(guān)聯(lián)到特定用戶。 如果你嘗試添加新主題, 將看到錯誤消息IntegrityError , 指出learning_logs_topic.user_id 不能為NULL 。 Django的意思是說, 創(chuàng)建新主題時, 你必須指定其owner 字段的值。
可以通過request 對象獲悉當(dāng)前用戶, 因此存在一個修復(fù)這種問題的簡單方案。 請?zhí)砑酉旅娴拇a, 將新主題關(guān)聯(lián)到當(dāng)前用戶:
views.py
--snip--
@login_required
def new_topic(request):
"""添加新主題"""
if request.method != 'POST':
# 未提交數(shù)據(jù): 創(chuàng)建一個新表單
form = TopicForm() # 如果請求方法不是POST, 請求就可能是GET, 因此我們需要返回一個空表單
else:
'''
if form.is_valid(): # 必須先通過檢查確定它們是有效的
form.save() # 表單中的數(shù)據(jù)寫入數(shù)據(jù)庫
# 函數(shù)reverse() 根據(jù)指定的URL模型確定URL, 這意味著Django將在頁面被請求時生成URL。
# 調(diào)用HttpResponseRedirect() 將用戶重定向到顯示新增條目所屬主題的頁面
return HttpResponseRedirect(reverse('learning_logs:topics'))
'''
if form.is_valid(): # 將新主題關(guān)聯(lián)到當(dāng)前用戶
new_topic = form.save(commit=False)
new_topic.owner = request.user
new_topic.save()
return HttpResponseRedirect(reverse('learning_logs:topics'))
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
--snip--
現(xiàn)在, 這個項目允許任何用戶注冊, 而每個用戶想添加多少新主題都可以。 每個用戶都只能訪問自己的數(shù)據(jù), 無論是查看數(shù)據(jù)、 輸入新數(shù)據(jù)還是修改舊數(shù)據(jù)時都如此。
GitHub鏈接:
https://github.com/lichangke/LeetCode
知乎個人首頁:
https://www.zhihu.com/people/lichangke/
簡書個人首頁:
http://www.lxweimin.com/u/3e95c7555dc7
個人Blog:
https://lichangke.github.io/
歡迎大家來一起交流學(xué)習(xí)