19.3 讓用戶擁有自己的數(shù)據(jù)
這一節(jié),我們將對(duì)一些頁面進(jìn)行限制,僅讓已登錄的用戶訪問它們,我們還將確保每個(gè)主題都屬于特定用戶。我們將創(chuàng)建一個(gè)系統(tǒng),確保各項(xiàng)數(shù)據(jù)所屬的用戶,再限制對(duì)頁面的訪問,讓用戶只能使用自己的數(shù)據(jù)。
在本節(jié)中,我們將修改模型Topic,讓每個(gè)主題都?xì)w屬于特定用戶。這也將影響條目,因?yàn)槊總€(gè)條目都屬于特定的主題。我們先來限制對(duì)一些頁面的訪問
19.3.1 使用@login_required限制訪問
Django提供了裝飾器@login_required,讓你能夠輕松地實(shí)現(xiàn)這樣的目標(biāo):對(duì)于某些頁面,只允許已登錄的用戶訪問它們。裝飾器(decorator)是放在函數(shù)定義前面的指令,Python在函數(shù)運(yùn)行前,根據(jù)它來修飾函數(shù)代碼的行為。下面來看一個(gè)示例
1. 限制對(duì)topics頁面的訪問
每個(gè)主題都?xì)w特定用戶所有,因此應(yīng)只允許已登錄的用戶請(qǐng)求topics頁面。為此,修改learning_logs/views.py 為如下代碼
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
# Create your views here.
def index(request):
return render(request, 'learning_logs/index.html')
@login_required
def topics(request):
"""show all topics"""
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':
# 未提交數(shù)據(jù): 創(chuàng)建一個(gè)新表單
form = TopicForm()
else:
# POST提交的數(shù)據(jù), 對(duì)數(shù)據(jù)進(jìn)行處理
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':
# 未提交數(shù)據(jù),創(chuàng)建一個(gè)空表單
form = EntryForm()
else:
# POST提交的數(shù)據(jù),對(duì)數(shù)據(jù)進(jìn)行處理
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':
# 初次請(qǐng)求,使用當(dāng)前條目填充表單
form = EntryForm(instance=entry)
else:
# POST提交表單,對(duì)數(shù)據(jù)進(jìn)行處理
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)
login_required()的代碼檢查用戶是否已登錄,僅當(dāng)用戶已登錄時(shí),Django才運(yùn)行topics()的代碼。
如果用戶未登錄,就重定向到登錄頁面。為實(shí)現(xiàn)這種重定向,我們需要修改settings.py,讓Django知道到哪里去查找登錄頁面。在settings.py末尾添加如下代碼:
# my settings
LOGIN_URL = '/users/login/'
現(xiàn)在,如果未登錄的用戶請(qǐng)求裝飾器@login_required的保護(hù)頁面,Django將重定向到settings.py中LOGIN_URL指定的URL。在這里單擊Topics鏈接,未登錄用戶將重定向到登錄頁面。
2. 全面限制對(duì)項(xiàng)目“學(xué)習(xí)筆記”的訪問
Django使我們能夠輕松的限制對(duì)頁面的訪問,但你必須針對(duì)要保護(hù)哪些頁面做出決定。最好先確定哪些頁面不需要保護(hù),再限制對(duì)其他所有頁面的訪問。你可以輕松地修改過于嚴(yán)格的訪問限制,其風(fēng)險(xiǎn)比不限制對(duì)敏感頁面的訪問更低。
在項(xiàng)目“學(xué)習(xí)筆記中”,我們將不限制對(duì)主頁、注冊(cè)頁面和注銷頁面的訪問,并限制對(duì)其他所有頁面的訪問。
在下面的learning_logs/views.py中,對(duì)除index()外的每個(gè)視圖都應(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--
19.3.2 將數(shù)據(jù)關(guān)聯(lián)到用戶
現(xiàn)在,需要將數(shù)據(jù)關(guān)聯(lián)到提交它們的用戶。我們只需將最高層的數(shù)據(jù)關(guān)聯(lián)到用戶,這樣更低層的數(shù)據(jù)將自動(dòng)關(guān)聯(lián)到用戶。例如,在項(xiàng)目“學(xué)習(xí)筆記”中,應(yīng)用程序的最高層數(shù)據(jù)是主題,而所有條目都與特定主題相關(guān)聯(lián)。只要每個(gè)主題都?xì)w屬于特定用戶,我們就能確定數(shù)據(jù)庫中每個(gè)條目的所有者。
下面來修改模型Topic,在其中添加一個(gè)關(guān)聯(lián)到用戶的外鍵。這樣做后,我們必須對(duì)數(shù)據(jù)庫進(jìn)行遷移。最后,我們必須對(duì)有些視圖進(jìn)行修改,使其只顯示與當(dāng)前登錄的用戶相關(guān)聯(lián)的數(shù)據(jù)。
1. 修改模型Topic
對(duì)models.py的修改只涉及兩行代碼:
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
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)
def __str__(self):
return self.text
class Entry(models.Model):
--snip--
我們先導(dǎo)入了django.contrib.auth中的模型User,然后在Topic中添加了字段owner,它建立到模型User的外鍵關(guān)系。
2. 確定當(dāng)前有哪些用戶
我們遷移數(shù)據(jù)庫時(shí),Django將對(duì)數(shù)據(jù)庫進(jìn)行修改,使其能夠存儲(chǔ)主題和用戶之間的關(guān)聯(lián)。為執(zhí)行遷移,Django需要知道該將各個(gè)既有主題關(guān)聯(lián)到哪些用戶。最簡單的辦法是,將既有主題都關(guān)聯(lián)到同一個(gè)用戶,如超級(jí)用戶。為此,我們需要知道該用戶的ID。
下面來查看已創(chuàng)建的所有用戶的ID。啟用Django shell回話,并執(zhí)行如下命令:
(ll_env) c5220056@GMPTIC:~/myworkplace$ python manage.py shell
Python 3.5.2 (default, Nov 23 2017, 16:37:01)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: ll_admin>, <User: testuser>, <User: Yolanda>]>
>>> for user in User.objects.all():
... print(user.username, user.id)
...
ll_admin 1
testuser 2
Yolanda 3
Django詢問要將既有主題關(guān)聯(lián)到哪個(gè)用戶時(shí),我們將指定其中的一個(gè)ID值。
3. 遷移數(shù)據(jù)庫
知道用戶ID后,就可以遷移數(shù)據(jù)庫了。
(ll_env) c5220056@GMPTIC:~/myworkplace$ python manage.py makemigrations 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
我們首先執(zhí)行了命令makemigrations,在輸出中,Django指出我們?cè)噲D給既有模型Topic添加一個(gè)必不可少(不可為空)的字段,而該字段沒有默認(rèn)值。Django提供了兩種選擇:1. 現(xiàn)在提供默認(rèn)值 2. 退出并在models.py中添加默認(rèn)值。
為將所有既有主題都關(guān)聯(lián)到管理用戶ll_admin, 我輸入了用戶ID值為1。接下來,Django使用這個(gè)值來遷移數(shù)據(jù)庫,并生成了遷移文件0003_topic_owner.py,它再模型Topic中添加字段owner。
現(xiàn)在可以執(zhí)行遷移了。
(ll_env) c5220056@GMPTIC:~/myworkplace$ 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
為驗(yàn)證遷移是否符合預(yù)期,可在shell會(huì)話中像下面這樣做
>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
... print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
注意:你可以重置數(shù)據(jù)庫而不是遷移它,但如果這樣做,既有的數(shù)據(jù)都將丟失。一種不錯(cuò)的做法是,學(xué)習(xí)如何在遷移數(shù)據(jù)庫的同時(shí)確保用戶數(shù)據(jù)的完整性。如果你確實(shí)想要一個(gè)全新的數(shù)據(jù)庫,可執(zhí)行命令python manage.py flush,這將重建數(shù)據(jù)庫結(jié)構(gòu)。如果這樣做,就必須重新創(chuàng)建超級(jí)用戶,且原來的所有數(shù)據(jù)都將丟失。
19.3.3 只允許用戶訪問自己的主題
當(dāng)前,不管是以哪個(gè)用戶的身份登錄,都能看到所有的主題。接下來我們使得它只向用戶顯示屬于自己的 主題。
在views.py中,對(duì)函數(shù)topics()做修改
@login_required
def topics(request):
"""show all topics"""
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
用戶登錄后,request對(duì)象將由一個(gè)user屬性,這個(gè)屬性存儲(chǔ)了有關(guān)該用戶的信息。代碼Topic.objects.filter(owner=request.user)讓Django只從數(shù)據(jù)庫中獲取owner屬性為當(dāng)前用戶的Topic對(duì)象。由于我們沒有修改主題的顯示方式,因此無需對(duì)頁面topics的模板做任何修改。
查看結(jié)果

19.3.4 保護(hù)用戶的主題
我們還沒有限制對(duì)顯示單個(gè)主題的頁面的訪問,因此任何已登錄的用戶都可輸入類似于http://localhost:8000/topics/1/的URL,來訪問顯示相應(yīng)主題的頁面。如下圖

為修復(fù)這個(gè)問題,我們?cè)谝晥D函數(shù)topic()獲取請(qǐng)求的條目前進(jìn)行檢查
# views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect, Http404
from django.urls import reverse
--snip--
@login_required
def topic(request, topic_id):
topic = Topic.objects.get(id=topic_id)
# 確認(rèn)請(qǐng)求的主題屬于當(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--
服務(wù)器上沒有請(qǐng)求的資源時(shí),標(biāo)準(zhǔn)的做法是返回404響應(yīng)。在這里,我們導(dǎo)入了異常Http404,并在用戶請(qǐng)求它不能查看的主題時(shí)引發(fā)這個(gè)異常。收到主題請(qǐng)求后,我們?cè)阡秩揪W(wǎng)頁前檢查該主題是否屬于當(dāng)前登錄的用戶。
現(xiàn)在,我們?cè)俨榭雌渌脩舻闹黝}條目時(shí),將看到Django發(fā)送的消息Page Not Found。

19.3.5 保護(hù)頁面edit_entry
頁面edit_entry 的URL為http://localhost:8000/edit_entry/entry_id / ,其中 entry_id 是一個(gè)數(shù)字。下面來保護(hù)這個(gè)頁面,禁止用戶通過輸入類似于前面的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:
raise Http404
if request.method != 'POST':
# 初次請(qǐng)求,使用當(dāng)前條目的內(nèi)容填充表單
--snip--
19.3.6 將新主題關(guān)聯(lián)到當(dāng)前用戶
當(dāng)前,用戶添加新主題的頁面存在問題,因?yàn)樗鼪]有將新主題關(guān)聯(lián)到特定用戶。如果你嘗試添加新主題,將看到錯(cuò)誤消息IntegrityError ,指出learning_logs_topic.user_id 不能為NULL 。Django的意思是說,創(chuàng)建新主題時(shí),你必須指定其owner 字段的值。
添加如下代碼,將新主題關(guān)聯(lián)到當(dāng)前用戶:
@login_required
def new_topic(request):
"""添加新主題"""
if request.method != 'POST':
# 未提交數(shù)據(jù): 創(chuàng)建一個(gè)新表單
form = TopicForm()
else:
# POST提交的數(shù)據(jù), 對(duì)數(shù)據(jù)進(jìn)行處理
form = TopicForm(request.POST)
if form.is_valid():
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)
我們首先調(diào)用form.save() ,并傳遞實(shí)參commit=False ,這是因?yàn)槲覀兿刃薷男轮黝},再將其保存到數(shù)據(jù)庫中。接下來,將新主題的owner 屬性設(shè)置為當(dāng)前用戶。最后,對(duì)剛定義的主題實(shí)例調(diào)用save() ?,F(xiàn)在主題包含所有必不可少的數(shù)據(jù),將被成功地保存。